courtdays 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- courtdays-0.1.0/LICENSE +21 -0
- courtdays-0.1.0/PKG-INFO +185 -0
- courtdays-0.1.0/README.md +156 -0
- courtdays-0.1.0/pyproject.toml +56 -0
- courtdays-0.1.0/setup.cfg +4 -0
- courtdays-0.1.0/src/courtdays/__init__.py +21 -0
- courtdays-0.1.0/src/courtdays/calendar.py +114 -0
- courtdays-0.1.0/src/courtdays/cli.py +164 -0
- courtdays-0.1.0/src/courtdays/compute.py +305 -0
- courtdays-0.1.0/src/courtdays.egg-info/PKG-INFO +185 -0
- courtdays-0.1.0/src/courtdays.egg-info/SOURCES.txt +15 -0
- courtdays-0.1.0/src/courtdays.egg-info/dependency_links.txt +1 -0
- courtdays-0.1.0/src/courtdays.egg-info/entry_points.txt +2 -0
- courtdays-0.1.0/src/courtdays.egg-info/requires.txt +6 -0
- courtdays-0.1.0/src/courtdays.egg-info/top_level.txt +1 -0
- courtdays-0.1.0/tests/test_cli.py +31 -0
- courtdays-0.1.0/tests/test_compute.py +89 -0
courtdays-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 SybilGambleyyu
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
courtdays-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: courtdays
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compute court and legal deadlines (FRCP Rule 6 style) with holiday calendars and audit trails
|
|
5
|
+
Author: SybilGambleyyu
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SybilGambleyyu/courtdays
|
|
8
|
+
Project-URL: Repository, https://github.com/SybilGambleyyu/courtdays
|
|
9
|
+
Project-URL: Issues, https://github.com/SybilGambleyyu/courtdays/issues
|
|
10
|
+
Keywords: legal,law,court,deadline,FRCP,Rule 6,business-days,paralegal,docket
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Legal Industry
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Office/Business
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: holidays>=0.40
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# courtdays
|
|
31
|
+
|
|
32
|
+
**Compute court and legal deadlines with an audit trail.**
|
|
33
|
+
|
|
34
|
+
FRCP Rule 6–style day counting for lawyers, paralegals, and docketing tools — not a black box, not a SaaS calendar. Every deadline comes with the steps that produced it so a human can check the math.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install courtdays
|
|
38
|
+
|
|
39
|
+
courtdays deadline 2024-06-03 30
|
|
40
|
+
courtdays deadline 2024-06-03 14 --service mail
|
|
41
|
+
courtdays deadline 2024-06-07 10 --mode business --calendar CA
|
|
42
|
+
courtdays check 2024-07-04
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Why this exists
|
|
46
|
+
|
|
47
|
+
Missed response deadlines are costly. Commercial docketing products (CourtDeadline, DocketBird, firm practice-management suites) handle this at enterprise prices and often as closed systems.
|
|
48
|
+
|
|
49
|
+
Open-source date libraries know weekends and holidays. Almost none implement the **legal counting rules** docket clerks actually use:
|
|
50
|
+
|
|
51
|
+
1. Exclude the day of the triggering event
|
|
52
|
+
2. Count intermediate days (calendar mode — modern FRCP Rule 6)
|
|
53
|
+
3. If the last day is a Saturday, Sunday, or legal holiday, continue to the next business day
|
|
54
|
+
4. Optionally add service-method days (e.g. +3 for mail under FRCP 6(d))
|
|
55
|
+
|
|
56
|
+
**courtdays** is that engine: installable, scriptable, and honest about what it does and does not decide.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install courtdays
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Requires Python 3.10+.
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
### FRCP-style calendar deadline
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
courtdays deadline 2024-06-03 30
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Example output:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Deadline: 2024-07-03 (Wednesday)
|
|
78
|
+
Trigger: 2024-06-03
|
|
79
|
+
Period: 30 day(s) [calendar]
|
|
80
|
+
Calendar: US-Federal
|
|
81
|
+
|
|
82
|
+
Computation steps:
|
|
83
|
+
1. Trigger date: 2024-06-03
|
|
84
|
+
2. Period: 30 day(s), mode=calendar, calendar=US-Federal
|
|
85
|
+
3. Exclude trigger day → counting starts 2024-06-04
|
|
86
|
+
4. Count 30 calendar day(s) inclusive from 2024-06-04 → raw last day 2024-07-03
|
|
87
|
+
5. Last day 2024-07-03 is a business day — final
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Mail service (+3)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
courtdays deadline 2024-06-03 14 --service mail
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Business days only (contracts / SLAs)
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
courtdays deadline 2024-06-07 5 --mode business
|
|
100
|
+
courtdays business 2024-06-07 5
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### State holiday calendars
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
courtdays deadline 2024-06-03 30 --calendar CA
|
|
107
|
+
courtdays check 2024-11-28 --calendar NY
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### JSON for tools
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
courtdays deadline 2024-06-03 30 --json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Python API
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from courtdays import compute_deadline, us_federal_calendar, us_state_calendar
|
|
120
|
+
|
|
121
|
+
result = compute_deadline(
|
|
122
|
+
"2024-06-03",
|
|
123
|
+
14,
|
|
124
|
+
service="mail",
|
|
125
|
+
calendar=us_federal_calendar(),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
print(result.deadline) # datetime.date
|
|
129
|
+
print(result.steps) # human-readable audit trail
|
|
130
|
+
print(result.to_dict()) # JSON-serializable
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Business-day helper
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from courtdays import add_business_days
|
|
137
|
+
|
|
138
|
+
due = add_business_days("2024-06-07", 3) # skips weekend
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Custom calendars
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from datetime import date
|
|
145
|
+
from courtdays import CourtCalendar, compute_deadline
|
|
146
|
+
|
|
147
|
+
cal = CourtCalendar(
|
|
148
|
+
name="MyCourt",
|
|
149
|
+
holiday_dates={date(2024, 7, 5)}, # local court closure
|
|
150
|
+
holiday_names={date(2024, 7, 5): "Local court holiday"},
|
|
151
|
+
)
|
|
152
|
+
result = compute_deadline("2024-06-15", 20, calendar=cal)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Counting rules (defaults)
|
|
156
|
+
|
|
157
|
+
| Setting | Default | Meaning |
|
|
158
|
+
|---------|---------|---------|
|
|
159
|
+
| `mode` | `calendar` | Count all intermediate days (Rule 6 style) |
|
|
160
|
+
| `exclude_trigger_day` | `True` | Day of event is not day 1 |
|
|
161
|
+
| `extend_if_nonbusiness` | `True` | Last day on weekend/holiday → next business day |
|
|
162
|
+
| `service=mail` | +3 days | FRCP 6(d) style add-on after the period |
|
|
163
|
+
| `service=electronic` | +0 | Modern e-service default (override with `--service-days`) |
|
|
164
|
+
|
|
165
|
+
**This is not legal advice.** Local rules, standing orders, and state codes differ (some still use older “less than 11 days” intermediate-weekend rules; some courts close on different days). Always verify against the controlling authority for the matter. courtdays makes the arithmetic explicit so that verification is faster.
|
|
166
|
+
|
|
167
|
+
## What this is not
|
|
168
|
+
|
|
169
|
+
- Not a full docketing / calendar product
|
|
170
|
+
- Not a substitute for counsel or the court clerk
|
|
171
|
+
- Not automatic multi-jurisdiction conflict detection
|
|
172
|
+
|
|
173
|
+
It is the date engine you can put under a checklist, a script, or a small firm’s internal tool.
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python -m venv .venv && source .venv/bin/activate
|
|
179
|
+
pip install -e ".[dev]"
|
|
180
|
+
pytest -q
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# courtdays
|
|
2
|
+
|
|
3
|
+
**Compute court and legal deadlines with an audit trail.**
|
|
4
|
+
|
|
5
|
+
FRCP Rule 6–style day counting for lawyers, paralegals, and docketing tools — not a black box, not a SaaS calendar. Every deadline comes with the steps that produced it so a human can check the math.
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pip install courtdays
|
|
9
|
+
|
|
10
|
+
courtdays deadline 2024-06-03 30
|
|
11
|
+
courtdays deadline 2024-06-03 14 --service mail
|
|
12
|
+
courtdays deadline 2024-06-07 10 --mode business --calendar CA
|
|
13
|
+
courtdays check 2024-07-04
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
## Why this exists
|
|
17
|
+
|
|
18
|
+
Missed response deadlines are costly. Commercial docketing products (CourtDeadline, DocketBird, firm practice-management suites) handle this at enterprise prices and often as closed systems.
|
|
19
|
+
|
|
20
|
+
Open-source date libraries know weekends and holidays. Almost none implement the **legal counting rules** docket clerks actually use:
|
|
21
|
+
|
|
22
|
+
1. Exclude the day of the triggering event
|
|
23
|
+
2. Count intermediate days (calendar mode — modern FRCP Rule 6)
|
|
24
|
+
3. If the last day is a Saturday, Sunday, or legal holiday, continue to the next business day
|
|
25
|
+
4. Optionally add service-method days (e.g. +3 for mail under FRCP 6(d))
|
|
26
|
+
|
|
27
|
+
**courtdays** is that engine: installable, scriptable, and honest about what it does and does not decide.
|
|
28
|
+
|
|
29
|
+
## Install
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install courtdays
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Requires Python 3.10+.
|
|
36
|
+
|
|
37
|
+
## CLI
|
|
38
|
+
|
|
39
|
+
### FRCP-style calendar deadline
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
courtdays deadline 2024-06-03 30
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Example output:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
Deadline: 2024-07-03 (Wednesday)
|
|
49
|
+
Trigger: 2024-06-03
|
|
50
|
+
Period: 30 day(s) [calendar]
|
|
51
|
+
Calendar: US-Federal
|
|
52
|
+
|
|
53
|
+
Computation steps:
|
|
54
|
+
1. Trigger date: 2024-06-03
|
|
55
|
+
2. Period: 30 day(s), mode=calendar, calendar=US-Federal
|
|
56
|
+
3. Exclude trigger day → counting starts 2024-06-04
|
|
57
|
+
4. Count 30 calendar day(s) inclusive from 2024-06-04 → raw last day 2024-07-03
|
|
58
|
+
5. Last day 2024-07-03 is a business day — final
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
### Mail service (+3)
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
courtdays deadline 2024-06-03 14 --service mail
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### Business days only (contracts / SLAs)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
courtdays deadline 2024-06-07 5 --mode business
|
|
71
|
+
courtdays business 2024-06-07 5
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### State holiday calendars
|
|
75
|
+
|
|
76
|
+
```bash
|
|
77
|
+
courtdays deadline 2024-06-03 30 --calendar CA
|
|
78
|
+
courtdays check 2024-11-28 --calendar NY
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### JSON for tools
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
courtdays deadline 2024-06-03 30 --json
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
## Python API
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
from courtdays import compute_deadline, us_federal_calendar, us_state_calendar
|
|
91
|
+
|
|
92
|
+
result = compute_deadline(
|
|
93
|
+
"2024-06-03",
|
|
94
|
+
14,
|
|
95
|
+
service="mail",
|
|
96
|
+
calendar=us_federal_calendar(),
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
print(result.deadline) # datetime.date
|
|
100
|
+
print(result.steps) # human-readable audit trail
|
|
101
|
+
print(result.to_dict()) # JSON-serializable
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### Business-day helper
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from courtdays import add_business_days
|
|
108
|
+
|
|
109
|
+
due = add_business_days("2024-06-07", 3) # skips weekend
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Custom calendars
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
from datetime import date
|
|
116
|
+
from courtdays import CourtCalendar, compute_deadline
|
|
117
|
+
|
|
118
|
+
cal = CourtCalendar(
|
|
119
|
+
name="MyCourt",
|
|
120
|
+
holiday_dates={date(2024, 7, 5)}, # local court closure
|
|
121
|
+
holiday_names={date(2024, 7, 5): "Local court holiday"},
|
|
122
|
+
)
|
|
123
|
+
result = compute_deadline("2024-06-15", 20, calendar=cal)
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
## Counting rules (defaults)
|
|
127
|
+
|
|
128
|
+
| Setting | Default | Meaning |
|
|
129
|
+
|---------|---------|---------|
|
|
130
|
+
| `mode` | `calendar` | Count all intermediate days (Rule 6 style) |
|
|
131
|
+
| `exclude_trigger_day` | `True` | Day of event is not day 1 |
|
|
132
|
+
| `extend_if_nonbusiness` | `True` | Last day on weekend/holiday → next business day |
|
|
133
|
+
| `service=mail` | +3 days | FRCP 6(d) style add-on after the period |
|
|
134
|
+
| `service=electronic` | +0 | Modern e-service default (override with `--service-days`) |
|
|
135
|
+
|
|
136
|
+
**This is not legal advice.** Local rules, standing orders, and state codes differ (some still use older “less than 11 days” intermediate-weekend rules; some courts close on different days). Always verify against the controlling authority for the matter. courtdays makes the arithmetic explicit so that verification is faster.
|
|
137
|
+
|
|
138
|
+
## What this is not
|
|
139
|
+
|
|
140
|
+
- Not a full docketing / calendar product
|
|
141
|
+
- Not a substitute for counsel or the court clerk
|
|
142
|
+
- Not automatic multi-jurisdiction conflict detection
|
|
143
|
+
|
|
144
|
+
It is the date engine you can put under a checklist, a script, or a small firm’s internal tool.
|
|
145
|
+
|
|
146
|
+
## Development
|
|
147
|
+
|
|
148
|
+
```bash
|
|
149
|
+
python -m venv .venv && source .venv/bin/activate
|
|
150
|
+
pip install -e ".[dev]"
|
|
151
|
+
pytest -q
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## License
|
|
155
|
+
|
|
156
|
+
MIT
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "courtdays"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Compute court and legal deadlines (FRCP Rule 6 style) with holiday calendars and audit trails"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
license-files = ["LICENSE"]
|
|
12
|
+
requires-python = ">=3.10"
|
|
13
|
+
authors = [{ name = "SybilGambleyyu" }]
|
|
14
|
+
keywords = [
|
|
15
|
+
"legal",
|
|
16
|
+
"law",
|
|
17
|
+
"court",
|
|
18
|
+
"deadline",
|
|
19
|
+
"FRCP",
|
|
20
|
+
"Rule 6",
|
|
21
|
+
"business-days",
|
|
22
|
+
"paralegal",
|
|
23
|
+
"docket",
|
|
24
|
+
]
|
|
25
|
+
classifiers = [
|
|
26
|
+
"Development Status :: 4 - Beta",
|
|
27
|
+
"Intended Audience :: Legal Industry",
|
|
28
|
+
"Intended Audience :: Developers",
|
|
29
|
+
"Programming Language :: Python :: 3",
|
|
30
|
+
"Programming Language :: Python :: 3.10",
|
|
31
|
+
"Programming Language :: Python :: 3.11",
|
|
32
|
+
"Programming Language :: Python :: 3.12",
|
|
33
|
+
"Programming Language :: Python :: 3.13",
|
|
34
|
+
"Topic :: Office/Business",
|
|
35
|
+
]
|
|
36
|
+
dependencies = [
|
|
37
|
+
"holidays>=0.40",
|
|
38
|
+
]
|
|
39
|
+
|
|
40
|
+
[project.optional-dependencies]
|
|
41
|
+
dev = ["pytest>=7.0", "build", "twine"]
|
|
42
|
+
|
|
43
|
+
[project.scripts]
|
|
44
|
+
courtdays = "courtdays.cli:main"
|
|
45
|
+
|
|
46
|
+
[project.urls]
|
|
47
|
+
Homepage = "https://github.com/SybilGambleyyu/courtdays"
|
|
48
|
+
Repository = "https://github.com/SybilGambleyyu/courtdays"
|
|
49
|
+
Issues = "https://github.com/SybilGambleyyu/courtdays/issues"
|
|
50
|
+
|
|
51
|
+
[tool.setuptools.packages.find]
|
|
52
|
+
where = ["src"]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
pythonpath = ["src"]
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
courtdays — compute legal and court deadlines with an auditable trail.
|
|
3
|
+
|
|
4
|
+
Implements FRCP Rule 6–style counting (exclude trigger day, count intermediate
|
|
5
|
+
days, extend if the last day is a weekend or legal holiday) plus pure business-
|
|
6
|
+
day arithmetic and optional service-method add-ons (e.g. +3 for mail).
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from courtdays.calendar import CourtCalendar, us_federal_calendar, us_state_calendar
|
|
10
|
+
from courtdays.compute import DeadlineResult, add_business_days, compute_deadline
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
__all__ = [
|
|
14
|
+
"CourtCalendar",
|
|
15
|
+
"us_federal_calendar",
|
|
16
|
+
"us_state_calendar",
|
|
17
|
+
"DeadlineResult",
|
|
18
|
+
"compute_deadline",
|
|
19
|
+
"add_business_days",
|
|
20
|
+
"__version__",
|
|
21
|
+
]
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""Holiday calendars for court and business-day computation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import date, timedelta
|
|
7
|
+
from typing import Iterable, Optional, Set, Union
|
|
8
|
+
|
|
9
|
+
import holidays as holidays_lib
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class CourtCalendar:
|
|
14
|
+
"""
|
|
15
|
+
A set of non-business (holiday) dates plus weekend rules.
|
|
16
|
+
|
|
17
|
+
Weekends default to Saturday and Sunday. Pass custom weekend weekdays
|
|
18
|
+
(0=Mon … 6=Sun) if a jurisdiction differs.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
name: str
|
|
22
|
+
holiday_dates: Set[date] = field(default_factory=set)
|
|
23
|
+
holiday_names: dict[date, str] = field(default_factory=dict)
|
|
24
|
+
weekend: Set[int] = field(default_factory=lambda: {5, 6}) # Sat, Sun
|
|
25
|
+
|
|
26
|
+
def is_weekend(self, d: date) -> bool:
|
|
27
|
+
return d.weekday() in self.weekend
|
|
28
|
+
|
|
29
|
+
def is_holiday(self, d: date) -> bool:
|
|
30
|
+
return d in self.holiday_dates
|
|
31
|
+
|
|
32
|
+
def is_business_day(self, d: date) -> bool:
|
|
33
|
+
return not self.is_weekend(d) and not self.is_holiday(d)
|
|
34
|
+
|
|
35
|
+
def holiday_name(self, d: date) -> Optional[str]:
|
|
36
|
+
return self.holiday_names.get(d)
|
|
37
|
+
|
|
38
|
+
def next_business_day(self, d: date) -> date:
|
|
39
|
+
cur = d
|
|
40
|
+
while not self.is_business_day(cur):
|
|
41
|
+
cur += timedelta(days=1)
|
|
42
|
+
return cur
|
|
43
|
+
|
|
44
|
+
def previous_business_day(self, d: date) -> date:
|
|
45
|
+
cur = d
|
|
46
|
+
while not self.is_business_day(cur):
|
|
47
|
+
cur -= timedelta(days=1)
|
|
48
|
+
return cur
|
|
49
|
+
|
|
50
|
+
def extend_range(self, years: Iterable[int]) -> None:
|
|
51
|
+
"""No-op hook for calendars that lazy-load by year; federal is preloaded."""
|
|
52
|
+
return None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _default_years() -> range:
|
|
56
|
+
# Wide window so historical and near-future dockets both work out of the box.
|
|
57
|
+
y = date.today().year
|
|
58
|
+
return range(y - 15, y + 16)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def us_federal_calendar(
|
|
62
|
+
years: Optional[Union[int, Iterable[int]]] = None,
|
|
63
|
+
) -> CourtCalendar:
|
|
64
|
+
"""
|
|
65
|
+
U.S. federal legal holidays (as observed), suitable for FRCP Rule 6(a)(6)
|
|
66
|
+
“legal holiday” treatment in federal court.
|
|
67
|
+
|
|
68
|
+
Includes New Year's, MLK Day, Washington's Birthday, Memorial Day,
|
|
69
|
+
Juneteenth, Independence Day, Labor Day, Columbus Day, Veterans Day,
|
|
70
|
+
Thanksgiving, and Christmas — with observed shifts when they fall on
|
|
71
|
+
weekends (per the `holidays` package / OPM-style observance).
|
|
72
|
+
"""
|
|
73
|
+
if years is None:
|
|
74
|
+
years = _default_years()
|
|
75
|
+
elif isinstance(years, int):
|
|
76
|
+
years = [years]
|
|
77
|
+
|
|
78
|
+
h = holidays_lib.country_holidays("US", years=list(years))
|
|
79
|
+
dates: set[date] = set(h.keys())
|
|
80
|
+
names = {d: str(n) for d, n in h.items()}
|
|
81
|
+
return CourtCalendar(
|
|
82
|
+
name="US-Federal",
|
|
83
|
+
holiday_dates=dates,
|
|
84
|
+
holiday_names=names,
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def us_state_calendar(
|
|
89
|
+
state: str,
|
|
90
|
+
years: Optional[Union[int, Iterable[int]]] = None,
|
|
91
|
+
) -> CourtCalendar:
|
|
92
|
+
"""
|
|
93
|
+
U.S. state holiday calendar (federal + state-specific where the holidays
|
|
94
|
+
package provides them). *state* is a two-letter code, e.g. ``"CA"``, ``"NY"``.
|
|
95
|
+
"""
|
|
96
|
+
if years is None:
|
|
97
|
+
years = _default_years()
|
|
98
|
+
elif isinstance(years, int):
|
|
99
|
+
years = [years]
|
|
100
|
+
|
|
101
|
+
code = state.strip().upper()
|
|
102
|
+
h = holidays_lib.country_holidays("US", subdiv=code, years=list(years))
|
|
103
|
+
dates: set[date] = set(h.keys())
|
|
104
|
+
names = {d: str(n) for d, n in h.items()}
|
|
105
|
+
return CourtCalendar(
|
|
106
|
+
name=f"US-{code}",
|
|
107
|
+
holiday_dates=dates,
|
|
108
|
+
holiday_names=names,
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def empty_calendar(name: str = "empty") -> CourtCalendar:
|
|
113
|
+
"""Weekends only — no holidays. Useful for pure weekend-aware math."""
|
|
114
|
+
return CourtCalendar(name=name)
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Command-line interface for courtdays."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from datetime import date
|
|
9
|
+
|
|
10
|
+
from courtdays import __version__
|
|
11
|
+
from courtdays.calendar import us_federal_calendar, us_state_calendar
|
|
12
|
+
from courtdays.compute import compute_deadline, parse_date
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="courtdays",
|
|
18
|
+
description=(
|
|
19
|
+
"Compute court/legal deadlines (FRCP Rule 6 style) with holiday "
|
|
20
|
+
"calendars and a step-by-step audit trail."
|
|
21
|
+
),
|
|
22
|
+
)
|
|
23
|
+
parser.add_argument("--version", action="version", version=f"courtdays {__version__}")
|
|
24
|
+
|
|
25
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
26
|
+
|
|
27
|
+
p = sub.add_parser("deadline", help="Compute a deadline from a trigger date")
|
|
28
|
+
p.add_argument("trigger", help="Trigger date (YYYY-MM-DD or MM/DD/YYYY)")
|
|
29
|
+
p.add_argument("days", type=int, help="Number of days in the period")
|
|
30
|
+
p.add_argument(
|
|
31
|
+
"--mode",
|
|
32
|
+
choices=["calendar", "business"],
|
|
33
|
+
default="calendar",
|
|
34
|
+
help="calendar = FRCP Rule 6 style (default); business = business days only",
|
|
35
|
+
)
|
|
36
|
+
p.add_argument(
|
|
37
|
+
"--calendar",
|
|
38
|
+
default="federal",
|
|
39
|
+
help="Holiday calendar: federal (default) or US state code e.g. CA, NY",
|
|
40
|
+
)
|
|
41
|
+
p.add_argument(
|
|
42
|
+
"--service",
|
|
43
|
+
choices=["none", "mail", "electronic", "personal", "other"],
|
|
44
|
+
default="none",
|
|
45
|
+
help="Service method (mail adds +3 calendar days under FRCP 6(d) default)",
|
|
46
|
+
)
|
|
47
|
+
p.add_argument(
|
|
48
|
+
"--service-days",
|
|
49
|
+
type=int,
|
|
50
|
+
default=None,
|
|
51
|
+
help="Override service extra days (default depends on --service)",
|
|
52
|
+
)
|
|
53
|
+
p.add_argument(
|
|
54
|
+
"--include-trigger",
|
|
55
|
+
action="store_true",
|
|
56
|
+
help="Count the trigger day as day 1 (default: exclude trigger day)",
|
|
57
|
+
)
|
|
58
|
+
p.add_argument(
|
|
59
|
+
"--no-extend",
|
|
60
|
+
action="store_true",
|
|
61
|
+
help="Do not extend when last day is weekend/holiday",
|
|
62
|
+
)
|
|
63
|
+
p.add_argument("--json", action="store_true", help="Emit JSON")
|
|
64
|
+
p.add_argument("-q", "--quiet", action="store_true", help="Print only the deadline date")
|
|
65
|
+
|
|
66
|
+
p_biz = sub.add_parser("business", help="Add N business days to a date")
|
|
67
|
+
p_biz.add_argument("start", help="Start date")
|
|
68
|
+
p_biz.add_argument("days", type=int, help="Business days to add")
|
|
69
|
+
p_biz.add_argument("--calendar", default="federal")
|
|
70
|
+
p_biz.add_argument("--json", action="store_true")
|
|
71
|
+
|
|
72
|
+
p_check = sub.add_parser("check", help="Is this date a business day?")
|
|
73
|
+
p_check.add_argument("date", help="Date to check")
|
|
74
|
+
p_check.add_argument("--calendar", default="federal")
|
|
75
|
+
|
|
76
|
+
args = parser.parse_args(argv)
|
|
77
|
+
|
|
78
|
+
if args.command == "deadline":
|
|
79
|
+
return _cmd_deadline(args)
|
|
80
|
+
if args.command == "business":
|
|
81
|
+
return _cmd_business(args)
|
|
82
|
+
if args.command == "check":
|
|
83
|
+
return _cmd_check(args)
|
|
84
|
+
return 2
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _load_calendar(spec: str):
|
|
88
|
+
s = (spec or "federal").strip().lower()
|
|
89
|
+
if s in {"federal", "us", "us-federal", "fed"}:
|
|
90
|
+
return us_federal_calendar()
|
|
91
|
+
if len(s) == 2:
|
|
92
|
+
return us_state_calendar(s.upper())
|
|
93
|
+
raise SystemExit(f"Unknown calendar {spec!r}; use 'federal' or a state code like CA")
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _cmd_deadline(args: argparse.Namespace) -> int:
|
|
97
|
+
cal = _load_calendar(args.calendar)
|
|
98
|
+
result = compute_deadline(
|
|
99
|
+
args.trigger,
|
|
100
|
+
args.days,
|
|
101
|
+
mode=args.mode,
|
|
102
|
+
calendar=cal,
|
|
103
|
+
exclude_trigger_day=not args.include_trigger,
|
|
104
|
+
extend_if_nonbusiness=not args.no_extend,
|
|
105
|
+
service=args.service,
|
|
106
|
+
service_extra_days=args.service_days,
|
|
107
|
+
)
|
|
108
|
+
if args.quiet:
|
|
109
|
+
print(result.deadline.isoformat())
|
|
110
|
+
return 0
|
|
111
|
+
if args.json:
|
|
112
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
113
|
+
return 0
|
|
114
|
+
|
|
115
|
+
print(f"Deadline: {result.deadline.isoformat()} ({result.deadline.strftime('%A')})")
|
|
116
|
+
print(f"Trigger: {result.trigger_date.isoformat()}")
|
|
117
|
+
print(f"Period: {result.period_days} day(s) [{result.mode}]")
|
|
118
|
+
print(f"Calendar: {result.calendar_name}")
|
|
119
|
+
if result.service_extra_days:
|
|
120
|
+
print(f"Service: {result.service_method} (+{result.service_extra_days})")
|
|
121
|
+
print()
|
|
122
|
+
print("Computation steps:")
|
|
123
|
+
for i, step in enumerate(result.steps, 1):
|
|
124
|
+
print(f" {i}. {step}")
|
|
125
|
+
print()
|
|
126
|
+
print(
|
|
127
|
+
"Disclaimer: not legal advice. Verify against the controlling rules, "
|
|
128
|
+
"local orders, and court holiday lists for your matter."
|
|
129
|
+
)
|
|
130
|
+
return 0
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _cmd_business(args: argparse.Namespace) -> int:
|
|
134
|
+
cal = _load_calendar(args.calendar)
|
|
135
|
+
result = compute_deadline(
|
|
136
|
+
args.start,
|
|
137
|
+
args.days,
|
|
138
|
+
mode="business",
|
|
139
|
+
calendar=cal,
|
|
140
|
+
exclude_trigger_day=True,
|
|
141
|
+
extend_if_nonbusiness=False,
|
|
142
|
+
)
|
|
143
|
+
if args.json:
|
|
144
|
+
print(json.dumps(result.to_dict(), indent=2))
|
|
145
|
+
else:
|
|
146
|
+
print(result.deadline.isoformat())
|
|
147
|
+
return 0
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _cmd_check(args: argparse.Namespace) -> int:
|
|
151
|
+
cal = _load_calendar(args.calendar)
|
|
152
|
+
d = parse_date(args.date)
|
|
153
|
+
biz = cal.is_business_day(d)
|
|
154
|
+
print(f"{d.isoformat()} ({d.strftime('%A')})")
|
|
155
|
+
print(f" calendar: {cal.name}")
|
|
156
|
+
print(f" weekend: {cal.is_weekend(d)}")
|
|
157
|
+
print(f" holiday: {cal.is_holiday(d)}"
|
|
158
|
+
+ (f" — {cal.holiday_name(d)}" if cal.is_holiday(d) else ""))
|
|
159
|
+
print(f" business day: {biz}")
|
|
160
|
+
return 0 if biz else 1
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
if __name__ == "__main__":
|
|
164
|
+
raise SystemExit(main())
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
"""Deadline computation engines."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from datetime import date, datetime, timedelta
|
|
7
|
+
from typing import Any, Literal, Optional, Union
|
|
8
|
+
|
|
9
|
+
from courtdays.calendar import CourtCalendar, us_federal_calendar
|
|
10
|
+
|
|
11
|
+
CountMode = Literal["calendar", "business"]
|
|
12
|
+
ServiceMethod = Literal["none", "mail", "electronic", "personal", "other"]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# FRCP Rule 6(d): when service is made under Rule 5(b)(2)(C), (D), (E), or (F)
|
|
16
|
+
# (mail, leaving with clerk, electronic if consented under older practice, etc.)
|
|
17
|
+
# — 3 days are added after the period would otherwise expire.
|
|
18
|
+
# Modern e-service often does NOT get +3; callers choose.
|
|
19
|
+
SERVICE_EXTRA_DAYS: dict[str, int] = {
|
|
20
|
+
"none": 0,
|
|
21
|
+
"personal": 0,
|
|
22
|
+
"electronic": 0, # default modern e-filing: no +3
|
|
23
|
+
"mail": 3,
|
|
24
|
+
"other": 0,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@dataclass
|
|
29
|
+
class DeadlineResult:
|
|
30
|
+
"""Full result of a deadline computation, including an audit trail."""
|
|
31
|
+
|
|
32
|
+
trigger_date: date
|
|
33
|
+
period_days: int
|
|
34
|
+
deadline: date
|
|
35
|
+
mode: str
|
|
36
|
+
calendar_name: str
|
|
37
|
+
service_method: str
|
|
38
|
+
service_extra_days: int
|
|
39
|
+
steps: list[str] = field(default_factory=list)
|
|
40
|
+
meta: dict[str, Any] = field(default_factory=dict)
|
|
41
|
+
|
|
42
|
+
def to_dict(self) -> dict[str, Any]:
|
|
43
|
+
return {
|
|
44
|
+
"trigger_date": self.trigger_date.isoformat(),
|
|
45
|
+
"period_days": self.period_days,
|
|
46
|
+
"deadline": self.deadline.isoformat(),
|
|
47
|
+
"mode": self.mode,
|
|
48
|
+
"calendar": self.calendar_name,
|
|
49
|
+
"service_method": self.service_method,
|
|
50
|
+
"service_extra_days": self.service_extra_days,
|
|
51
|
+
"steps": list(self.steps),
|
|
52
|
+
"meta": dict(self.meta),
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def parse_date(value: Union[str, date, datetime]) -> date:
|
|
57
|
+
if isinstance(value, datetime):
|
|
58
|
+
return value.date()
|
|
59
|
+
if isinstance(value, date):
|
|
60
|
+
return value
|
|
61
|
+
s = str(value).strip()
|
|
62
|
+
for fmt in ("%Y-%m-%d", "%m/%d/%Y", "%m/%d/%y", "%Y/%m/%d", "%d-%b-%Y", "%b %d, %Y"):
|
|
63
|
+
try:
|
|
64
|
+
return datetime.strptime(s, fmt).date()
|
|
65
|
+
except ValueError:
|
|
66
|
+
continue
|
|
67
|
+
raise ValueError(f"Cannot parse date: {value!r}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def compute_deadline(
|
|
71
|
+
trigger: Union[str, date, datetime],
|
|
72
|
+
days: int,
|
|
73
|
+
*,
|
|
74
|
+
mode: CountMode = "calendar",
|
|
75
|
+
calendar: Optional[CourtCalendar] = None,
|
|
76
|
+
exclude_trigger_day: bool = True,
|
|
77
|
+
extend_if_nonbusiness: bool = True,
|
|
78
|
+
service: ServiceMethod = "none",
|
|
79
|
+
service_extra_days: Optional[int] = None,
|
|
80
|
+
) -> DeadlineResult:
|
|
81
|
+
"""
|
|
82
|
+
Compute a legal deadline.
|
|
83
|
+
|
|
84
|
+
**FRCP Rule 6–style calendar mode** (default ``mode="calendar"``):
|
|
85
|
+
|
|
86
|
+
1. Start from the trigger event date (e.g. date of service / order).
|
|
87
|
+
2. Exclude the trigger day itself when ``exclude_trigger_day`` is True.
|
|
88
|
+
3. Count every calendar day (weekends and holidays included as intermediate days).
|
|
89
|
+
4. If the last day is a Saturday, Sunday, or legal holiday and
|
|
90
|
+
``extend_if_nonbusiness`` is True, continue to the next business day.
|
|
91
|
+
5. Optionally add service-method days (FRCP 6(d) mail = +3 after the period).
|
|
92
|
+
|
|
93
|
+
**Business-day mode** (``mode="business"``):
|
|
94
|
+
|
|
95
|
+
Count only business days (skip weekends and holidays). Common for contract
|
|
96
|
+
SLAs and some administrative deadlines — *not* the general FRCP day-count rule.
|
|
97
|
+
|
|
98
|
+
Parameters
|
|
99
|
+
----------
|
|
100
|
+
trigger:
|
|
101
|
+
Date the period starts from (day of the event that triggers the clock).
|
|
102
|
+
days:
|
|
103
|
+
Number of days in the period (must be >= 0).
|
|
104
|
+
mode:
|
|
105
|
+
``"calendar"`` (Rule 6 style) or ``"business"``.
|
|
106
|
+
calendar:
|
|
107
|
+
Holiday calendar. Defaults to U.S. federal legal holidays.
|
|
108
|
+
exclude_trigger_day:
|
|
109
|
+
If True (default), counting begins the day *after* the trigger.
|
|
110
|
+
extend_if_nonbusiness:
|
|
111
|
+
If True (default), when the raw last day is a weekend/holiday, move to
|
|
112
|
+
the next business day.
|
|
113
|
+
service:
|
|
114
|
+
Named service method; sets default extra days (mail → 3).
|
|
115
|
+
service_extra_days:
|
|
116
|
+
Override the extra days added after the period (FRCP 6(d) style).
|
|
117
|
+
"""
|
|
118
|
+
if days < 0:
|
|
119
|
+
raise ValueError("days must be >= 0")
|
|
120
|
+
|
|
121
|
+
cal = calendar or us_federal_calendar()
|
|
122
|
+
trigger_d = parse_date(trigger)
|
|
123
|
+
steps: list[str] = []
|
|
124
|
+
steps.append(f"Trigger date: {trigger_d.isoformat()}")
|
|
125
|
+
steps.append(f"Period: {days} day(s), mode={mode}, calendar={cal.name}")
|
|
126
|
+
|
|
127
|
+
if service_extra_days is None:
|
|
128
|
+
service_extra_days = SERVICE_EXTRA_DAYS.get(service, 0)
|
|
129
|
+
else:
|
|
130
|
+
service_extra_days = int(service_extra_days)
|
|
131
|
+
|
|
132
|
+
if mode == "calendar":
|
|
133
|
+
deadline, mode_steps = _count_calendar(
|
|
134
|
+
trigger_d,
|
|
135
|
+
days,
|
|
136
|
+
cal,
|
|
137
|
+
exclude_trigger_day=exclude_trigger_day,
|
|
138
|
+
extend_if_nonbusiness=extend_if_nonbusiness,
|
|
139
|
+
)
|
|
140
|
+
elif mode == "business":
|
|
141
|
+
deadline, mode_steps = _count_business(
|
|
142
|
+
trigger_d,
|
|
143
|
+
days,
|
|
144
|
+
cal,
|
|
145
|
+
exclude_trigger_day=exclude_trigger_day,
|
|
146
|
+
)
|
|
147
|
+
else:
|
|
148
|
+
raise ValueError(f"Unknown mode: {mode!r}")
|
|
149
|
+
steps.extend(mode_steps)
|
|
150
|
+
|
|
151
|
+
if service_extra_days:
|
|
152
|
+
before = deadline
|
|
153
|
+
# FRCP 6(d): 3 days added after the period would otherwise expire.
|
|
154
|
+
# Those 3 are calendar days, then extend if landing on non-business.
|
|
155
|
+
deadline = before + timedelta(days=service_extra_days)
|
|
156
|
+
steps.append(
|
|
157
|
+
f"Service method {service!r}: add {service_extra_days} day(s) "
|
|
158
|
+
f"→ {deadline.isoformat()}"
|
|
159
|
+
)
|
|
160
|
+
if extend_if_nonbusiness and not cal.is_business_day(deadline):
|
|
161
|
+
extended = cal.next_business_day(deadline)
|
|
162
|
+
reason = _nonbusiness_reason(cal, deadline)
|
|
163
|
+
steps.append(
|
|
164
|
+
f"Service-adjusted last day {deadline.isoformat()} is {reason}; "
|
|
165
|
+
f"extended to {extended.isoformat()}"
|
|
166
|
+
)
|
|
167
|
+
deadline = extended
|
|
168
|
+
|
|
169
|
+
return DeadlineResult(
|
|
170
|
+
trigger_date=trigger_d,
|
|
171
|
+
period_days=days,
|
|
172
|
+
deadline=deadline,
|
|
173
|
+
mode=mode,
|
|
174
|
+
calendar_name=cal.name,
|
|
175
|
+
service_method=service,
|
|
176
|
+
service_extra_days=service_extra_days,
|
|
177
|
+
steps=steps,
|
|
178
|
+
meta={
|
|
179
|
+
"exclude_trigger_day": exclude_trigger_day,
|
|
180
|
+
"extend_if_nonbusiness": extend_if_nonbusiness,
|
|
181
|
+
},
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def add_business_days(
|
|
186
|
+
start: Union[str, date, datetime],
|
|
187
|
+
days: int,
|
|
188
|
+
*,
|
|
189
|
+
calendar: Optional[CourtCalendar] = None,
|
|
190
|
+
exclude_start: bool = True,
|
|
191
|
+
) -> date:
|
|
192
|
+
"""Convenience: pure business-day addition, returns the date only."""
|
|
193
|
+
result = compute_deadline(
|
|
194
|
+
start,
|
|
195
|
+
days,
|
|
196
|
+
mode="business",
|
|
197
|
+
calendar=calendar,
|
|
198
|
+
exclude_trigger_day=exclude_start,
|
|
199
|
+
extend_if_nonbusiness=False,
|
|
200
|
+
service="none",
|
|
201
|
+
)
|
|
202
|
+
return result.deadline
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def _count_calendar(
|
|
206
|
+
trigger: date,
|
|
207
|
+
days: int,
|
|
208
|
+
cal: CourtCalendar,
|
|
209
|
+
*,
|
|
210
|
+
exclude_trigger_day: bool,
|
|
211
|
+
extend_if_nonbusiness: bool,
|
|
212
|
+
) -> tuple[date, list[str]]:
|
|
213
|
+
steps: list[str] = []
|
|
214
|
+
if days == 0:
|
|
215
|
+
deadline = trigger if not exclude_trigger_day else trigger
|
|
216
|
+
steps.append("Zero-day period; deadline is the trigger date")
|
|
217
|
+
if extend_if_nonbusiness and not cal.is_business_day(deadline):
|
|
218
|
+
ext = cal.next_business_day(deadline)
|
|
219
|
+
steps.append(
|
|
220
|
+
f"Zero-day falls on {_nonbusiness_reason(cal, deadline)}; "
|
|
221
|
+
f"extended to {ext.isoformat()}"
|
|
222
|
+
)
|
|
223
|
+
deadline = ext
|
|
224
|
+
return deadline, steps
|
|
225
|
+
|
|
226
|
+
# Exclude trigger day: first counted day is trigger+1
|
|
227
|
+
start = trigger + timedelta(days=1) if exclude_trigger_day else trigger
|
|
228
|
+
if exclude_trigger_day:
|
|
229
|
+
steps.append(
|
|
230
|
+
f"Exclude trigger day → counting starts {start.isoformat()}"
|
|
231
|
+
)
|
|
232
|
+
else:
|
|
233
|
+
steps.append(f"Include trigger day → counting starts {start.isoformat()}")
|
|
234
|
+
|
|
235
|
+
# Count `days` calendar days from start inclusive
|
|
236
|
+
# If start is day 1, deadline is start + (days - 1)
|
|
237
|
+
deadline = start + timedelta(days=days - 1)
|
|
238
|
+
steps.append(
|
|
239
|
+
f"Count {days} calendar day(s) inclusive from {start.isoformat()} "
|
|
240
|
+
f"→ raw last day {deadline.isoformat()}"
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
if extend_if_nonbusiness and not cal.is_business_day(deadline):
|
|
244
|
+
ext = cal.next_business_day(deadline)
|
|
245
|
+
steps.append(
|
|
246
|
+
f"Last day {deadline.isoformat()} is {_nonbusiness_reason(cal, deadline)}; "
|
|
247
|
+
f"period continues to next business day {ext.isoformat()}"
|
|
248
|
+
)
|
|
249
|
+
deadline = ext
|
|
250
|
+
elif cal.is_business_day(deadline):
|
|
251
|
+
steps.append(f"Last day {deadline.isoformat()} is a business day — final")
|
|
252
|
+
|
|
253
|
+
return deadline, steps
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def _count_business(
|
|
257
|
+
trigger: date,
|
|
258
|
+
days: int,
|
|
259
|
+
cal: CourtCalendar,
|
|
260
|
+
*,
|
|
261
|
+
exclude_trigger_day: bool,
|
|
262
|
+
) -> tuple[date, list[str]]:
|
|
263
|
+
steps: list[str] = []
|
|
264
|
+
if days == 0:
|
|
265
|
+
d = trigger
|
|
266
|
+
if not cal.is_business_day(d):
|
|
267
|
+
d = cal.next_business_day(d)
|
|
268
|
+
steps.append(f"Zero business days; snap to business day {d.isoformat()}")
|
|
269
|
+
else:
|
|
270
|
+
steps.append(f"Zero business days; deadline {d.isoformat()}")
|
|
271
|
+
return d, steps
|
|
272
|
+
|
|
273
|
+
cur = trigger
|
|
274
|
+
if exclude_trigger_day:
|
|
275
|
+
cur = trigger + timedelta(days=1)
|
|
276
|
+
steps.append(f"Exclude trigger day → begin search at {cur.isoformat()}")
|
|
277
|
+
|
|
278
|
+
# Advance to first business day if needed when include-trigger?
|
|
279
|
+
# For exclude path: start counting business days from `cur` forward
|
|
280
|
+
counted = 0
|
|
281
|
+
# If we land on non-business at start of counting, skip forward first
|
|
282
|
+
while not cal.is_business_day(cur):
|
|
283
|
+
cur += timedelta(days=1)
|
|
284
|
+
|
|
285
|
+
while counted < days:
|
|
286
|
+
if cal.is_business_day(cur):
|
|
287
|
+
counted += 1
|
|
288
|
+
if counted == days:
|
|
289
|
+
break
|
|
290
|
+
cur += timedelta(days=1)
|
|
291
|
+
|
|
292
|
+
steps.append(
|
|
293
|
+
f"Counted {days} business day(s) → {cur.isoformat()}"
|
|
294
|
+
)
|
|
295
|
+
return cur, steps
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def _nonbusiness_reason(cal: CourtCalendar, d: date) -> str:
|
|
299
|
+
parts = []
|
|
300
|
+
if cal.is_weekend(d):
|
|
301
|
+
parts.append(d.strftime("%A"))
|
|
302
|
+
if cal.is_holiday(d):
|
|
303
|
+
name = cal.holiday_name(d) or "legal holiday"
|
|
304
|
+
parts.append(name)
|
|
305
|
+
return " / ".join(parts) if parts else "non-business day"
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: courtdays
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Compute court and legal deadlines (FRCP Rule 6 style) with holiday calendars and audit trails
|
|
5
|
+
Author: SybilGambleyyu
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/SybilGambleyyu/courtdays
|
|
8
|
+
Project-URL: Repository, https://github.com/SybilGambleyyu/courtdays
|
|
9
|
+
Project-URL: Issues, https://github.com/SybilGambleyyu/courtdays/issues
|
|
10
|
+
Keywords: legal,law,court,deadline,FRCP,Rule 6,business-days,paralegal,docket
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Legal Industry
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Topic :: Office/Business
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
License-File: LICENSE
|
|
23
|
+
Requires-Dist: holidays>=0.40
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0; extra == "dev"
|
|
26
|
+
Requires-Dist: build; extra == "dev"
|
|
27
|
+
Requires-Dist: twine; extra == "dev"
|
|
28
|
+
Dynamic: license-file
|
|
29
|
+
|
|
30
|
+
# courtdays
|
|
31
|
+
|
|
32
|
+
**Compute court and legal deadlines with an audit trail.**
|
|
33
|
+
|
|
34
|
+
FRCP Rule 6–style day counting for lawyers, paralegals, and docketing tools — not a black box, not a SaaS calendar. Every deadline comes with the steps that produced it so a human can check the math.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install courtdays
|
|
38
|
+
|
|
39
|
+
courtdays deadline 2024-06-03 30
|
|
40
|
+
courtdays deadline 2024-06-03 14 --service mail
|
|
41
|
+
courtdays deadline 2024-06-07 10 --mode business --calendar CA
|
|
42
|
+
courtdays check 2024-07-04
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Why this exists
|
|
46
|
+
|
|
47
|
+
Missed response deadlines are costly. Commercial docketing products (CourtDeadline, DocketBird, firm practice-management suites) handle this at enterprise prices and often as closed systems.
|
|
48
|
+
|
|
49
|
+
Open-source date libraries know weekends and holidays. Almost none implement the **legal counting rules** docket clerks actually use:
|
|
50
|
+
|
|
51
|
+
1. Exclude the day of the triggering event
|
|
52
|
+
2. Count intermediate days (calendar mode — modern FRCP Rule 6)
|
|
53
|
+
3. If the last day is a Saturday, Sunday, or legal holiday, continue to the next business day
|
|
54
|
+
4. Optionally add service-method days (e.g. +3 for mail under FRCP 6(d))
|
|
55
|
+
|
|
56
|
+
**courtdays** is that engine: installable, scriptable, and honest about what it does and does not decide.
|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install courtdays
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Requires Python 3.10+.
|
|
65
|
+
|
|
66
|
+
## CLI
|
|
67
|
+
|
|
68
|
+
### FRCP-style calendar deadline
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
courtdays deadline 2024-06-03 30
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
Example output:
|
|
75
|
+
|
|
76
|
+
```
|
|
77
|
+
Deadline: 2024-07-03 (Wednesday)
|
|
78
|
+
Trigger: 2024-06-03
|
|
79
|
+
Period: 30 day(s) [calendar]
|
|
80
|
+
Calendar: US-Federal
|
|
81
|
+
|
|
82
|
+
Computation steps:
|
|
83
|
+
1. Trigger date: 2024-06-03
|
|
84
|
+
2. Period: 30 day(s), mode=calendar, calendar=US-Federal
|
|
85
|
+
3. Exclude trigger day → counting starts 2024-06-04
|
|
86
|
+
4. Count 30 calendar day(s) inclusive from 2024-06-04 → raw last day 2024-07-03
|
|
87
|
+
5. Last day 2024-07-03 is a business day — final
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Mail service (+3)
|
|
91
|
+
|
|
92
|
+
```bash
|
|
93
|
+
courtdays deadline 2024-06-03 14 --service mail
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
### Business days only (contracts / SLAs)
|
|
97
|
+
|
|
98
|
+
```bash
|
|
99
|
+
courtdays deadline 2024-06-07 5 --mode business
|
|
100
|
+
courtdays business 2024-06-07 5
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### State holiday calendars
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
courtdays deadline 2024-06-03 30 --calendar CA
|
|
107
|
+
courtdays check 2024-11-28 --calendar NY
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### JSON for tools
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
courtdays deadline 2024-06-03 30 --json
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## Python API
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
from courtdays import compute_deadline, us_federal_calendar, us_state_calendar
|
|
120
|
+
|
|
121
|
+
result = compute_deadline(
|
|
122
|
+
"2024-06-03",
|
|
123
|
+
14,
|
|
124
|
+
service="mail",
|
|
125
|
+
calendar=us_federal_calendar(),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
print(result.deadline) # datetime.date
|
|
129
|
+
print(result.steps) # human-readable audit trail
|
|
130
|
+
print(result.to_dict()) # JSON-serializable
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Business-day helper
|
|
134
|
+
|
|
135
|
+
```python
|
|
136
|
+
from courtdays import add_business_days
|
|
137
|
+
|
|
138
|
+
due = add_business_days("2024-06-07", 3) # skips weekend
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Custom calendars
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from datetime import date
|
|
145
|
+
from courtdays import CourtCalendar, compute_deadline
|
|
146
|
+
|
|
147
|
+
cal = CourtCalendar(
|
|
148
|
+
name="MyCourt",
|
|
149
|
+
holiday_dates={date(2024, 7, 5)}, # local court closure
|
|
150
|
+
holiday_names={date(2024, 7, 5): "Local court holiday"},
|
|
151
|
+
)
|
|
152
|
+
result = compute_deadline("2024-06-15", 20, calendar=cal)
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
## Counting rules (defaults)
|
|
156
|
+
|
|
157
|
+
| Setting | Default | Meaning |
|
|
158
|
+
|---------|---------|---------|
|
|
159
|
+
| `mode` | `calendar` | Count all intermediate days (Rule 6 style) |
|
|
160
|
+
| `exclude_trigger_day` | `True` | Day of event is not day 1 |
|
|
161
|
+
| `extend_if_nonbusiness` | `True` | Last day on weekend/holiday → next business day |
|
|
162
|
+
| `service=mail` | +3 days | FRCP 6(d) style add-on after the period |
|
|
163
|
+
| `service=electronic` | +0 | Modern e-service default (override with `--service-days`) |
|
|
164
|
+
|
|
165
|
+
**This is not legal advice.** Local rules, standing orders, and state codes differ (some still use older “less than 11 days” intermediate-weekend rules; some courts close on different days). Always verify against the controlling authority for the matter. courtdays makes the arithmetic explicit so that verification is faster.
|
|
166
|
+
|
|
167
|
+
## What this is not
|
|
168
|
+
|
|
169
|
+
- Not a full docketing / calendar product
|
|
170
|
+
- Not a substitute for counsel or the court clerk
|
|
171
|
+
- Not automatic multi-jurisdiction conflict detection
|
|
172
|
+
|
|
173
|
+
It is the date engine you can put under a checklist, a script, or a small firm’s internal tool.
|
|
174
|
+
|
|
175
|
+
## Development
|
|
176
|
+
|
|
177
|
+
```bash
|
|
178
|
+
python -m venv .venv && source .venv/bin/activate
|
|
179
|
+
pip install -e ".[dev]"
|
|
180
|
+
pytest -q
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## License
|
|
184
|
+
|
|
185
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
LICENSE
|
|
2
|
+
README.md
|
|
3
|
+
pyproject.toml
|
|
4
|
+
src/courtdays/__init__.py
|
|
5
|
+
src/courtdays/calendar.py
|
|
6
|
+
src/courtdays/cli.py
|
|
7
|
+
src/courtdays/compute.py
|
|
8
|
+
src/courtdays.egg-info/PKG-INFO
|
|
9
|
+
src/courtdays.egg-info/SOURCES.txt
|
|
10
|
+
src/courtdays.egg-info/dependency_links.txt
|
|
11
|
+
src/courtdays.egg-info/entry_points.txt
|
|
12
|
+
src/courtdays.egg-info/requires.txt
|
|
13
|
+
src/courtdays.egg-info/top_level.txt
|
|
14
|
+
tests/test_cli.py
|
|
15
|
+
tests/test_compute.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
courtdays
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import json
|
|
2
|
+
|
|
3
|
+
from courtdays.cli import main
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def test_cli_deadline_quiet(capsys):
|
|
7
|
+
rc = main(["deadline", "2024-06-03", "30", "-q", "--calendar", "federal"])
|
|
8
|
+
assert rc == 0
|
|
9
|
+
out = capsys.readouterr().out.strip()
|
|
10
|
+
assert out == "2024-07-03"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_cli_deadline_json(capsys):
|
|
14
|
+
rc = main(["deadline", "2024-06-03", "14", "--json", "--service", "mail"])
|
|
15
|
+
assert rc == 0
|
|
16
|
+
data = json.loads(capsys.readouterr().out)
|
|
17
|
+
assert data["service_extra_days"] == 3
|
|
18
|
+
assert "deadline" in data
|
|
19
|
+
assert data["steps"]
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def test_cli_check_weekend(capsys):
|
|
23
|
+
rc = main(["check", "2024-06-08"]) # Saturday
|
|
24
|
+
assert rc == 1
|
|
25
|
+
assert "business day: False" in capsys.readouterr().out
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def test_cli_business(capsys):
|
|
29
|
+
rc = main(["business", "2024-06-07", "1"])
|
|
30
|
+
assert rc == 0
|
|
31
|
+
assert capsys.readouterr().out.strip() == "2024-06-10"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
from datetime import date, timedelta
|
|
2
|
+
|
|
3
|
+
from courtdays import compute_deadline, us_federal_calendar, add_business_days
|
|
4
|
+
from courtdays.calendar import empty_calendar
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_simple_30_day_period_no_holiday_end():
|
|
8
|
+
# Trigger Monday 2024-06-03; +30 calendar days, exclude trigger
|
|
9
|
+
# start 2024-06-04, +29 = 2024-07-03 Wednesday
|
|
10
|
+
r = compute_deadline("2024-06-03", 30, calendar=us_federal_calendar(2024))
|
|
11
|
+
assert r.deadline == date(2024, 7, 3)
|
|
12
|
+
assert r.deadline.strftime("%A") == "Wednesday"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_last_day_saturday_extends_to_monday():
|
|
16
|
+
# Find a period whose raw last day is Saturday
|
|
17
|
+
# Trigger Fri 2024-06-07; exclude → start Sat 6/8; 1 day → Sat 6/8 → Mon 6/10
|
|
18
|
+
r = compute_deadline("2024-06-07", 1, calendar=us_federal_calendar(2024))
|
|
19
|
+
assert r.deadline == date(2024, 6, 10)
|
|
20
|
+
assert any("Saturday" in s or "extended" in s.lower() for s in r.steps)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def test_independence_day_extension():
|
|
24
|
+
# If last day is July 4 holiday, extend
|
|
25
|
+
# Trigger 2024-06-04; 30 days: start 6/5 +29 = 7/4 Thursday holiday 2024
|
|
26
|
+
# 2024-07-04 is Thursday Independence Day → next business 2024-07-05
|
|
27
|
+
r = compute_deadline("2024-06-04", 30, calendar=us_federal_calendar(2024))
|
|
28
|
+
assert r.deadline == date(2024, 7, 5)
|
|
29
|
+
assert any("Independence" in s or "holiday" in s.lower() or "extended" in s.lower() for s in r.steps)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def test_mail_service_adds_three():
|
|
33
|
+
r = compute_deadline(
|
|
34
|
+
"2024-06-03",
|
|
35
|
+
14,
|
|
36
|
+
service="mail",
|
|
37
|
+
calendar=us_federal_calendar(2024),
|
|
38
|
+
)
|
|
39
|
+
base = compute_deadline("2024-06-03", 14, calendar=us_federal_calendar(2024))
|
|
40
|
+
# mail adds 3 calendar days then possibly extends
|
|
41
|
+
assert r.deadline >= base.deadline + timedelta(days=3) or r.service_extra_days == 3
|
|
42
|
+
assert r.service_extra_days == 3
|
|
43
|
+
assert r.deadline >= base.deadline
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def test_business_day_mode_skips_weekend():
|
|
47
|
+
# Trigger Friday; 1 business day → Monday
|
|
48
|
+
r = compute_deadline(
|
|
49
|
+
"2024-06-07", # Friday
|
|
50
|
+
1,
|
|
51
|
+
mode="business",
|
|
52
|
+
calendar=empty_calendar(),
|
|
53
|
+
)
|
|
54
|
+
assert r.deadline == date(2024, 6, 10) # Monday
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_add_business_days_helper():
|
|
58
|
+
d = add_business_days("2024-06-07", 2, calendar=empty_calendar())
|
|
59
|
+
assert d == date(2024, 6, 11) # Fri→Mon=1, Tue=2
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def test_include_trigger_day():
|
|
63
|
+
r = compute_deadline(
|
|
64
|
+
"2024-06-03",
|
|
65
|
+
1,
|
|
66
|
+
exclude_trigger_day=False,
|
|
67
|
+
calendar=empty_calendar(),
|
|
68
|
+
extend_if_nonbusiness=False,
|
|
69
|
+
)
|
|
70
|
+
assert r.deadline == date(2024, 6, 3)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def test_audit_trail_present():
|
|
74
|
+
r = compute_deadline("2024-01-02", 10, calendar=us_federal_calendar(2024))
|
|
75
|
+
assert len(r.steps) >= 3
|
|
76
|
+
d = r.to_dict()
|
|
77
|
+
assert d["deadline"] == r.deadline.isoformat()
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def test_zero_days():
|
|
81
|
+
r = compute_deadline("2024-06-03", 0, calendar=empty_calendar())
|
|
82
|
+
assert r.deadline == date(2024, 6, 3)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def test_negative_days_rejected():
|
|
86
|
+
import pytest
|
|
87
|
+
|
|
88
|
+
with pytest.raises(ValueError):
|
|
89
|
+
compute_deadline("2024-06-03", -1)
|