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.
@@ -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.
@@ -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,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,2 @@
1
+ [console_scripts]
2
+ courtdays = courtdays.cli:main
@@ -0,0 +1,6 @@
1
+ holidays>=0.40
2
+
3
+ [dev]
4
+ pytest>=7.0
5
+ build
6
+ twine
@@ -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)