forked from ChelseaKR/permit-bearings
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathclocks.py
More file actions
115 lines (93 loc) · 4.57 KB
/
Copy pathclocks.py
File metadata and controls
115 lines (93 loc) · 4.57 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
"""Statutory review-clock calculator.
Computes the deadlines State ADU Law and SB 9 impose on permitting
agencies, so an applicant (or staff) can see exactly where an application
stands. Encodes:
- Gov. Code § 66317(a)(2)(A): written completeness determination within
15 BUSINESS days of receipt (ADU/JADU).
- Gov. Code § 66317(a)(2)(F): the application is DEEMED COMPLETE if no
timely written determination is made.
- Gov. Code §§ 66317(a)(3), 66335(a)(3): approve or deny within 60
CALENDAR days of a complete application (existing SF/MF dwelling on
the lot); same 60-day clock for SB 9 (§§ 65852.21, 66411.7 per the
April 2026 HCD fact sheet).
Business days exclude weekends and California state holidays (Gov. Code
§ 6700 list, as observed by state offices: Saturday holidays shift to
Friday, Sunday holidays to Monday). Jurisdictions may observe additional
local holidays — a deployment should confirm the local calendar; the
state list is the floor.
The current `adu_clocks` helper models the bounded case in which the
application is complete on initial receipt. It does not model an incompleteness
notice, cure/resubmittal date, applicant delay, or tolling; production output
must collect those events separately.
"""
from __future__ import annotations
from dataclasses import dataclass
from datetime import date, timedelta
COMPLETENESS_BUSINESS_DAYS = 15 # § 66317(a)(2)(A)
DECISION_CALENDAR_DAYS = 60 # §§ 66317(a)(3), 66335(a)(3); SB 9
def _nth_weekday(year: int, month: int, weekday: int, n: int) -> date:
d = date(year, month, 1)
offset = (weekday - d.weekday()) % 7
return d + timedelta(days=offset + 7 * (n - 1))
def _last_weekday(year: int, month: int, weekday: int) -> date:
d = date(year + (month == 12), (month % 12) + 1, 1) - timedelta(days=1)
return d - timedelta(days=(d.weekday() - weekday) % 7)
def _observed(d: date) -> date:
if d.weekday() == 5: # Saturday -> Friday
return d - timedelta(days=1)
if d.weekday() == 6: # Sunday -> Monday
return d + timedelta(days=1)
return d
def ca_holidays(year: int) -> set[date]:
"""California state holidays per Gov. Code § 6700 as observed by state
offices (including the day after Thanksgiving)."""
thanksgiving = _nth_weekday(year, 11, 3, 4)
fixed = [date(year, 1, 1), date(year, 3, 31), date(year, 7, 4),
date(year, 11, 11), date(year, 12, 25)]
floating = [
_nth_weekday(year, 1, 0, 3), # Martin Luther King Jr. Day
_nth_weekday(year, 2, 0, 3), # Presidents' Day
_last_weekday(year, 5, 0), # Memorial Day
_nth_weekday(year, 9, 0, 1), # Labor Day
thanksgiving,
thanksgiving + timedelta(days=1), # day after Thanksgiving
]
return {_observed(d) for d in fixed} | set(floating)
def add_business_days(start: date, days: int,
holidays: set[date] | None = None) -> date:
if holidays is None:
holidays = ca_holidays(start.year) | ca_holidays(start.year + 1)
current = start
remaining = days
while remaining > 0:
current += timedelta(days=1)
if current.weekday() < 5 and current not in holidays:
remaining -= 1
return current
@dataclass(frozen=True)
class ClockStatus:
received: date
completeness_deadline: date # written determination due
deemed_complete_if_silent: date # day after deadline passes with no notice
decision_deadline_if_complete: date
def summary(self) -> str:
return "\n".join([
f"Application received: {self.received.isoformat()}",
f"Completeness notice due by: {self.completeness_deadline.isoformat()}"
" (Gov. Code § 66317(a)(2)(A), 15 business days)",
f"Deemed complete if no notice by: {self.deemed_complete_if_silent.isoformat()}"
" (Gov. Code § 66317(a)(2)(F))",
f"Decision due (assuming complete on receipt): "
f"{self.decision_deadline_if_complete.isoformat()}"
" (Gov. Code § 66317(a)(3), 60 days)",
])
def adu_clocks(received: date) -> ClockStatus:
"""Illustrate both deadlines assuming the application is complete on
initial receipt. Correction cycles require a separate completion event."""
completeness = add_business_days(received, COMPLETENESS_BUSINESS_DAYS)
return ClockStatus(
received=received,
completeness_deadline=completeness,
deemed_complete_if_silent=completeness + timedelta(days=1),
decision_deadline_if_complete=received + timedelta(days=DECISION_CALENDAR_DAYS),
)