Retention policies

A RetentionPolicy is the per-purpose rule the engine resolves against: how long to keep data, from when, what to do at erasure, and whether another law holds it.

from dpdpstack import RetentionPolicy, Action, Trigger

RetentionPolicy(
    purpose="marketing",
    retention_days=730,                 # keep for 2 years…
    trigger=Trigger.SINCE_LAST_ACTIVITY,  # …from last activity (or SINCE_GRANT)
    action=Action.DELETE,               # delete vs ANONYMIZE at erasure
    legal_hold_days=0,                  # >0 means erasure defers under a hold
    legal_basis="",                     # recorded for audit when held
)

Fields

FieldMeaning
purposeThe purpose this rule applies to (required).
retention_daysHow long to keep data. 0 means no time-based retention.
triggerTrigger.SINCE_GRANT or Trigger.SINCE_LAST_ACTIVITY.
actionAction.DELETE or Action.ANONYMIZE.
legal_hold_daysIf greater than zero, erasure is deferred for this many days.
legal_basisThe cited law; recorded in the audit log and certificate.

Built-in presets

For the common Indian retention-vs-deletion conflicts, use a preset instead of hand-writing the rule:

from dpdpstack import rbi_kyc, pmla, cert_in_logs

rbi_kyc("kyc")              # KYC: 5-year hold, anonymize  - RBI KYC Master Direction
pmla("financial_records")  # transaction/identity records - PMLA
cert_in_logs("logs")       # system logs: 180-day hold     - CERT-In Directions

Each returns a fully-formed RetentionPolicy with the right legal_hold_days, action, and legal_basis filled in:

PresetHoldActionBasis recorded
rbi_kyc1825 days (5y)anonymizeRBI KYC Master Direction (5 years)
pmla1825 daysanonymizePMLA (retain transaction records)
cert_in_logs180 daysanonymizeCERT-In Directions (180-day log retention)

Custom policies

Build your own for any sector rule - for example, a 7-year tax/Companies-Act hold on invoices that anonymizes rather than deletes:

invoices = RetentionPolicy(
    purpose="invoices",
    retention_days=2555,          # ~7 years
    action=Action.ANONYMIZE,
    legal_hold_days=2555,
    legal_basis="Companies Act 2013 - books of account (8 years)",
)

Lint & score your policies

lint_policy statically checks a policy for compliance smells - a legal hold with no recorded basis, a hold that will hard-delete a regulated row, a basis cited without a hold period, retention far past what's justified - each tied to a DPDP citation.

from dpdpstack import RetentionPolicy, Action, lint_policy

lint_policy(RetentionPolicy(purpose="kyc", legal_hold_days=1825, action=Action.DELETE))
# [ERROR E001: … no legal_basis recorded …,
#  WARNING W001: … action=delete will hard-delete … consider action=anonymize …]

score_policies rolls the findings across all your policies into a graded readiness report - a deterministic 0–100 score, letter grade, and tier:

from dpdpstack import score_policies, rbi_kyc, pmla

score_policies([rbi_kyc(), pmla()]).summary
# '100/100 (A+, exemplary) across 2 policies: 2 clean, 0 errors, 0 warnings.'

From the shell (exit code is non-zero if any error is found, so it drops into CI):

dpdpstack lint --presets --score
# OK   kyc: no findings  …  Readiness: 100/100 (A+, exemplary) across 5 policies …

On the hosted platform, GET /readiness scores your configured policies and the dashboard shows the grade - the same linter, so SDK and platform never disagree.

DPDPStack is tooling, not legal advice - you set the retention periods and bases that fit your obligations.