Calculating California Public Records Act Deadlines in Python

Within State Law Compliance Frameworks, the California Public Records Act (CPRA) is the jurisdiction that most often trips automated deadline math, because its determination window is counted in calendar days while the two other large state acts a records team routinely handles — the Texas Public Information Act and New York’s FOIL — run on business days. Getting that one distinction wrong produces a clock that is off by several days in exactly the direction that causes a missed statutory deadline. This page builds a small, dependency-free Python module that computes the CPRA determination deadline correctly, applies the 14-day unusual-circumstances extension, rolls a due date off a weekend or state holiday, and writes an audit-ready record for every calculation.

The CPRA Statutory Clock

Under California Government Code § 7922.535(a), an agency that receives a request for records must, within 10 calendar days, determine whether the request seeks disclosable records in its possession and notify the requester of that determination and the reasons for it. The determination window is not a deadline to produce the records; it is a deadline to decide and communicate. Production follows “promptly” once the agency has made its determination. That two-step structure is the first thing an automation must model, because a system that treats “day 10” as the delivery date will misreport the agency’s posture to every requester.

The 10-day count runs on calendar days, so weekends and holidays inside the window do not pause it. Section 7922.535(b) permits an agency, in “unusual circumstances,” to extend the determination window by written notice for up to 14 additional calendar days — a maximum of 24 calendar days from receipt. Unusual circumstances are narrowly defined (records held offsite, voluminous separate-and-distinct records, or the need to consult another agency), and the extension must be justified in writing, so the automation should treat the extension as an explicit, logged decision rather than a default.

One rule keeps the calculation honest even though it is calendar-day based: when the computed determination date lands on a Saturday, Sunday, or state holiday — a day the agency is not open to receive or act on the determination — the deadline rolls forward to the next business day. Modeling that roll-forward is where a holiday calendar re-enters an otherwise calendar-day calculation. Escalation and reminder logic downstream in Deadline Tracking & Escalation depends on this record being both correct and explainable.

Prerequisites & Environment Setup

The module is pure standard library so it can run inline in the request path with no third-party runtime dependency and no supply-chain surface.

  • Python 3.11+ — for datetime.date, frozenset[date] typing, and dataclass(frozen=True, slots=True).
  • Standard library only: dataclasses, datetime, json, logging. No external calendar library is required; the state holiday set is a small, reviewable constant.
  • A version-controlled holiday set — California’s statutory holidays under Gov. Code § 6700 change little year to year, but you should pin one frozenset per calendar year and review it against the published schedule, exactly as you would any compliance artifact.
  • A structured log sink — stdout JSON in development, forwarded to your append-only store or SIEM in production so every computed deadline is reconstructable on appeal.

Keep the holiday set beside the code under version control. A deadline miscomputed because a holiday was silently dropped is indistinguishable, on audit, from an agency that simply ignored the statute.

Modeling the Determination Window

The diagram below shows the single request as two possible clocks: the standard 10-calendar-day determination window and the extended 24-day window when § 7922.535(b) applies, with the weekend/holiday roll-over noted underneath.

California Public Records Act determination timeline A left-to-right timeline. Day zero is the request received date, when the ten-calendar-day clock starts. At day ten the determination is due under Government Code section 7922.535 subsection a. When unusual circumstances apply, subsection b allows up to fourteen additional calendar days, moving the determination to day twenty-four. A note states that if a due date lands on a weekend or California state holiday it rolls to the next business day. DAY 0 Request received clock starts DAY 10 · § 7922.535(a) Determination due 10 calendar days DAY 24 · § 7922.535(b) Extended determination unusual circumstances +10 calendar days +14 day extension If a due date lands on a weekend or California state holiday, it rolls to the next business day.

The base determination date and the rolled determination date are recorded separately in the module below, so an auditor can see both the raw statutory count and the day the agency was actually obligated to act.

Step-by-Step Implementation

The module defines a frozen result record, a state holiday set, a roll-forward helper, and the deadline function itself. The comments cite the specific subsection each line implements.

python
from __future__ import annotations

import json
import logging
from dataclasses import dataclass
from datetime import date, datetime, timedelta, timezone


class JsonAuditFormatter(logging.Formatter):
    """Emit one structured JSON object per audit event (NIST SP 800-53 AU-3)."""

    def format(self, record: logging.LogRecord) -> str:
        payload = {
            "ts": datetime.now(timezone.utc).isoformat(),
            "level": record.levelname,
            "event": record.getMessage(),
        }
        for key in ("request_id", "statute", "received", "deadline"):
            if hasattr(record, key):
                payload[key] = getattr(record, key)
        return json.dumps(payload)


logger = logging.getLogger("cpra_deadline")
logger.setLevel(logging.INFO)
_handler = logging.StreamHandler()
_handler.setFormatter(JsonAuditFormatter())
logger.addHandler(_handler)


# California state holidays observed under Cal. Gov. Code § 6700 (2026 instances).
# The CPRA determination window is counted in *calendar* days, but a deadline that
# lands on one of these days (or a weekend) rolls forward to the next business day.
CA_STATE_HOLIDAYS_2026: frozenset[date] = frozenset({
    date(2026, 1, 1),    # New Year's Day
    date(2026, 1, 19),   # Dr. Martin Luther King Jr. Day
    date(2026, 2, 16),   # Presidents' Day
    date(2026, 3, 31),   # Cesar Chavez Day — § 6700(a)(16)
    date(2026, 5, 25),   # Memorial Day
    date(2026, 7, 3),    # Independence Day (observed)
    date(2026, 9, 7),    # Labor Day
    date(2026, 11, 11),  # Veterans Day
    date(2026, 11, 26),  # Thanksgiving Day
    date(2026, 11, 27),  # Day after Thanksgiving
    date(2026, 12, 25),  # Christmas Day
})


@dataclass(frozen=True, slots=True)
class CpraDeadline:
    """Immutable record of one CPRA determination calculation."""
    request_id: str
    received: date
    statute: str
    base_determination: date      # raw calendar-day count, before roll-forward
    unusual_circumstances: bool
    determination_due: date        # the day the agency is actually obligated to act


def next_business_day(day: date, holidays: frozenset[date]) -> date:
    """Advance to the next weekday that is not a state holiday."""
    while day.weekday() >= 5 or day in holidays:
        day += timedelta(days=1)
    return day


def cpra_determination_deadline(
    request_id: str,
    received: date,
    holidays: frozenset[date] = CA_STATE_HOLIDAYS_2026,
    unusual_circumstances: bool = False,
) -> CpraDeadline:
    """Compute the CPRA determination deadline for a single request.

    Cal. Gov. Code § 7922.535(a): the agency must determine whether the request
    seeks disclosable records within 10 *calendar* days of receipt.
    § 7922.535(b): unusual circumstances may extend that window by up to 14 days.
    A deadline falling on a weekend or § 6700 holiday rolls to the next business day.
    """
    try:
        if not isinstance(received, date):
            raise TypeError("received must be a datetime.date")

        # 10 calendar days from receipt — NOT business days (contrast TX PIA / NY FOIL).
        base = received + timedelta(days=10)
        deadline = base
        if unusual_circumstances:
            # § 7922.535(b): up to 14 additional calendar days, by written notice.
            deadline = deadline + timedelta(days=14)

        # Roll a weekend/holiday landing forward; the clock never expires on a closed day.
        determination_due = next_business_day(deadline, holidays)

        record = CpraDeadline(
            request_id=request_id,
            received=received,
            statute="Cal. Gov. Code § 7922.535",
            base_determination=base,
            unusual_circumstances=unusual_circumstances,
            determination_due=determination_due,
        )
        logger.info(
            "cpra_deadline_computed",
            extra={
                "request_id": request_id,
                "statute": record.statute,
                "received": received.isoformat(),
                "deadline": determination_due.isoformat(),
            },
        )
        return record
    except Exception:
        # Fail loud: a miscomputed statutory clock is itself a CPRA violation.
        logger.error("cpra_deadline_failed", extra={"request_id": request_id})
        raise

The function returns a structured record rather than a bare date because downstream reminder, escalation, and audit logic all need the surrounding context — the raw count, the roll-forward, and whether the extension was invoked — not just the final day.

Validation & Verification

Assert the two conventions that most often drift: the calendar-day base count, and the roll-forward off a closed day. The example below uses a request received Thursday, June 25, 2026, whose base determination lands on Sunday, July 5.

python
if __name__ == "__main__":
    standard = cpra_determination_deadline("CA-2026-0142", date(2026, 6, 25))
    # Base determination 2026-07-05 falls on a Sunday, so it rolls to Monday 2026-07-06.
    assert standard.base_determination == date(2026, 7, 5)
    assert standard.determination_due == date(2026, 7, 6)

    extended = cpra_determination_deadline(
        "CA-2026-0142", date(2026, 6, 25), unusual_circumstances=True
    )
    # § 7922.535(b): +14 calendar days -> 2026-07-19 (Sunday) rolls to 2026-07-20.
    assert extended.determination_due == date(2026, 7, 20)
    print("CPRA deadline checks passed")

Because CpraDeadline is frozen, replaying the same request during recovery yields an identical record and never starts a second statutory clock — the idempotency property an auditor expects. For broader confidence, replay a corpus of historical requests spanning each holiday and diff the computed deadlines against a manually verified table before any production deploy. The append-only trail this produces feeds naturally into the Audit Logging Architecture that oversight bodies read.

Troubleshooting & Edge Cases

  • Treating day 10 as the delivery date. The 10-day window is a determination deadline, not a production deadline. Diagnosis: requesters told records will arrive on day 10 that instead arrive later. Fix: model the determination and the “prompt” production as distinct events; only the determination is fixed by § 7922.535(a).
  • Counting the determination window in business days. Applying business-day math to the CPRA window overstates the deadline by two or more days. Diagnosis: California deadlines that mysteriously match the Texas or New York figures. Fix: the base count is calendar days; business days re-enter only for the roll-forward. The distinction is drawn explicitly against the Texas Public Information Act deadline model and the New York FOIL deadline model.
  • Applying the extension by default. The 14-day extension requires written notice and a qualifying unusual circumstance. Diagnosis: every request quietly carrying a 24-day window. Fix: require unusual_circumstances=True to be set by an explicit, logged decision, never inferred.
  • A holiday missing from the set. If Cesar Chavez Day or another § 6700 observance is absent, a deadline can land on a day the agency is closed. Diagnosis: a determination due date one business day early. Fix: pin and review the holiday frozenset per year; assert coverage in tests.
  • Ambiguous receipt date. A request received after business hours or over a weekend may have a receipt date the agency logs differently than the requester expects. Fix: normalize the receipt date upstream at intake and treat it as authoritative for the clock.

Compliance Verification Checklist

FAQ

Does the California Public Records Act count calendar days or business days?

The 10-day determination window under Gov. Code § 7922.535(a) is counted in calendar days — weekends and holidays inside the window do not pause it. Business days re-enter only at the edge: if the computed determination date lands on a Saturday, Sunday, or state holiday, it rolls forward to the next business day. This is the opposite convention from the Texas Public Information Act and New York FOIL, which count the window itself in business days, and mixing the two is the most common CPRA deadline bug.

How long is the unusual-circumstances extension, and when does it apply?

Section 7922.535(b) allows up to 14 additional calendar days, for a maximum of 24 calendar days from receipt. It applies only in the narrow situations the statute lists — records stored offsite, voluminous separate-and-distinct records, or the need to consult another agency — and the agency must give the requester written notice of the extension and its basis. Because it is a justified, communicated decision, the module requires it to be set explicitly rather than defaulting it on.

What happens if the determination deadline falls on a state holiday?

It rolls forward to the next day the agency is open. The next_business_day helper skips weekends and any date in the § 6700 holiday set, so a base determination on, say, Cesar Chavez Day advances to the following business day. Keeping that holiday set accurate and version-controlled is essential: a missing observance produces a deadline one business day early, which reads on audit as an agency acting outside its statutory window.