zerotime 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.
- zerotime-0.1.0/.gitignore +46 -0
- zerotime-0.1.0/CHANGELOG.md +20 -0
- zerotime-0.1.0/LICENSE +21 -0
- zerotime-0.1.0/PKG-INFO +258 -0
- zerotime-0.1.0/README.md +233 -0
- zerotime-0.1.0/docs/ANTI_PATTERNS.md +145 -0
- zerotime-0.1.0/docs/API_REFERENCE.md +720 -0
- zerotime-0.1.0/docs/ARCHITECTURE.md +147 -0
- zerotime-0.1.0/docs/DEVELOPMENT.md +94 -0
- zerotime-0.1.0/docs/FUNCTIONAL_ANALYSIS.md +198 -0
- zerotime-0.1.0/examples/01_basic_usage.py +103 -0
- zerotime-0.1.0/examples/02_dsl_syntax.py +167 -0
- zerotime-0.1.0/examples/03_rule_combination.py +174 -0
- zerotime-0.1.0/examples/04_generation_methods.py +160 -0
- zerotime-0.1.0/examples/05_navigation.py +197 -0
- zerotime-0.1.0/examples/06_timezones.py +227 -0
- zerotime-0.1.0/examples/07_configuration.py +230 -0
- zerotime-0.1.0/examples/08_json_serialization.py +213 -0
- zerotime-0.1.0/examples/09_builder_methods.py +212 -0
- zerotime-0.1.0/examples/10_real_world_examples.py +471 -0
- zerotime-0.1.0/examples/README.md +118 -0
- zerotime-0.1.0/pyproject.toml +158 -0
- zerotime-0.1.0/src/zerotime/__init__.py +37 -0
- zerotime-0.1.0/src/zerotime/core.py +1253 -0
- zerotime-0.1.0/src/zerotime/py.typed +0 -0
- zerotime-0.1.0/tests/test_config.py +243 -0
- zerotime-0.1.0/tests/test_core.py +793 -0
- zerotime-0.1.0/tests/test_edge_cases.py +505 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Python artifacts
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.egg-info/
|
|
5
|
+
|
|
6
|
+
# Build artifacts
|
|
7
|
+
dist/
|
|
8
|
+
build/
|
|
9
|
+
.coverage
|
|
10
|
+
htmlcov/
|
|
11
|
+
|
|
12
|
+
# All hidden directories (starts with .)
|
|
13
|
+
.*/
|
|
14
|
+
|
|
15
|
+
# Keep .gitignore and .github
|
|
16
|
+
!.gitignore
|
|
17
|
+
!.github/
|
|
18
|
+
|
|
19
|
+
# Hidden files to ignore
|
|
20
|
+
.DS_Store
|
|
21
|
+
.coverage
|
|
22
|
+
.env
|
|
23
|
+
.run.sh
|
|
24
|
+
.setup.sh
|
|
25
|
+
.setup.bat
|
|
26
|
+
.check_quality.py
|
|
27
|
+
.mypy_cache/
|
|
28
|
+
|
|
29
|
+
# Other files
|
|
30
|
+
*.swp
|
|
31
|
+
*.swo
|
|
32
|
+
*.log
|
|
33
|
+
*.bak
|
|
34
|
+
|
|
35
|
+
# Local dev files
|
|
36
|
+
demo_gradio.py
|
|
37
|
+
run.sh
|
|
38
|
+
.python-version
|
|
39
|
+
|
|
40
|
+
# Virtual environments (non-hidden)
|
|
41
|
+
env/
|
|
42
|
+
venv/
|
|
43
|
+
/.scripts/
|
|
44
|
+
|
|
45
|
+
# Lock files (library, not application)
|
|
46
|
+
uv.lock
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.0] - 2025-02-19
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Initial release
|
|
13
|
+
- Core Rule engine for datetime patterns
|
|
14
|
+
- Atomic rules and combined rules
|
|
15
|
+
- Recurring event support
|
|
16
|
+
- Rule validation and error handling
|
|
17
|
+
- Comprehensive test suite
|
|
18
|
+
- Full type hints with py.typed marker
|
|
19
|
+
|
|
20
|
+
[0.1.0]: https://github.com/francescofavi/zerotime/releases/tag/v0.1.0
|
zerotime-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Francesco Favi
|
|
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.
|
zerotime-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: zerotime
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: A Python datetime rule engine for working with recurring events and time patterns
|
|
5
|
+
Project-URL: Homepage, https://github.com/francescofavi/zerotime
|
|
6
|
+
Project-URL: Repository, https://github.com/francescofavi/zerotime
|
|
7
|
+
Project-URL: Issues, https://github.com/francescofavi/zerotime/issues
|
|
8
|
+
Project-URL: Documentation, https://github.com/francescofavi/zerotime#readme
|
|
9
|
+
Project-URL: Changelog, https://github.com/francescofavi/zerotime/blob/main/CHANGELOG.md
|
|
10
|
+
Author-email: Francesco Favi <14098835+francescofavi@users.noreply.github.com>
|
|
11
|
+
License: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: datetime,events,patterns,recurring,rules,scheduling,time
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Typing :: Typed
|
|
23
|
+
Requires-Python: >=3.11
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# Zerotime
|
|
27
|
+
|
|
28
|
+
[](https://github.com/francescofavi/zerotime/actions/workflows/ci.yml)
|
|
29
|
+
[](https://pypi.org/project/zerotime/)
|
|
30
|
+
[](https://pypi.org/project/zerotime/)
|
|
31
|
+
[](LICENSE)
|
|
32
|
+
|
|
33
|
+
A Python datetime rule engine for defining and working with recurring time patterns. Zerotime lets you express complex scheduling rules declaratively using a simple DSL (Domain-Specific Language), then query for matching datetimes, generate sequences, or combine rules using set operations.
|
|
34
|
+
|
|
35
|
+
## Why Zerotime?
|
|
36
|
+
|
|
37
|
+
Working with recurring events in datetime is surprisingly complex. You might need "every Monday at 9 AM", "the last day of each month", or "business hours except lunch break". Traditional approaches involve writing custom logic for each pattern, handling edge cases like leap years, varying month lengths, and weekday calculations.
|
|
38
|
+
|
|
39
|
+
Zerotime solves this by providing a unified `Rule` abstraction. Rules can be atomic (based on month, day, hour constraints) or combined using operators (`+` for union, `&` for intersection, `-` for difference). The library handles all the complexity of calendar math internally, provides memory-efficient generation of large date ranges, and includes JSON serialization for persistence.
|
|
40
|
+
|
|
41
|
+
## Technical Design
|
|
42
|
+
|
|
43
|
+
Zerotime uses **only the Python standard library** (no external dependencies). The core design principles:
|
|
44
|
+
|
|
45
|
+
- **Declarative DSL**: Each temporal field (months, days, weekdays, hours, minutes, seconds) accepts string expressions like `"1..5"` (range), `"/15"` (step), `"1,15,-1"` (list with last-day), or `"1..12,!7,!8"` (exclusions)
|
|
46
|
+
- **Composable Rules**: Rules combine via Python operators, enabling complex schedules from simple building blocks
|
|
47
|
+
- **Lazy Generation**: The `generate()` method yields datetimes on demand, suitable for large date ranges
|
|
48
|
+
- **Immutable Rules**: All `with_*` methods return new rule instances; original rules are never modified
|
|
49
|
+
- **Timezone Support**: Rules optionally bind to a timezone, with proper DST handling and validation of timezone-aware vs naive datetimes
|
|
50
|
+
- **Thread Safety**: Parsed expressions are cached with double-checked locking for safe concurrent use
|
|
51
|
+
|
|
52
|
+
## Components Overview
|
|
53
|
+
|
|
54
|
+
The library consists of three main components:
|
|
55
|
+
|
|
56
|
+
- **AtomicRule**: The fundamental building block. Defines temporal constraints using DSL expressions for each datetime field. All constraints must match (AND logic).
|
|
57
|
+
|
|
58
|
+
- **CombinedRule**: Created by combining two rules with an operator. Supports union (either matches), intersection (both match), and difference (first matches but not second).
|
|
59
|
+
|
|
60
|
+
- **RuleConfig**: Global configuration controlling search limits, generation caps, JSON size limits, and batch sizes for memory-efficient processing.
|
|
61
|
+
|
|
62
|
+
## Usage Example
|
|
63
|
+
|
|
64
|
+
Here's a realistic example showing how to define business working hours excluding lunch breaks and holidays:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from datetime import datetime, UTC
|
|
68
|
+
from zerotime import AtomicRule
|
|
69
|
+
|
|
70
|
+
# Business hours: weekdays 9-17, on the hour
|
|
71
|
+
business_hours = AtomicRule(
|
|
72
|
+
weekdays="1..5", # Monday to Friday
|
|
73
|
+
hours="9..17", # 9 AM to 5 PM
|
|
74
|
+
minutes="0", # On the hour
|
|
75
|
+
seconds="0",
|
|
76
|
+
timezone=UTC
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Lunch break: 12-13
|
|
80
|
+
lunch_break = AtomicRule(
|
|
81
|
+
weekdays="1..5",
|
|
82
|
+
hours="12,13",
|
|
83
|
+
minutes="0",
|
|
84
|
+
seconds="0",
|
|
85
|
+
timezone=UTC
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
# Working hours = business hours minus lunch
|
|
89
|
+
working_hours = business_hours - lunch_break
|
|
90
|
+
|
|
91
|
+
# Find the next working hour from now
|
|
92
|
+
now = datetime(2025, 1, 15, 10, 30, 0, tzinfo=UTC)
|
|
93
|
+
next_slot = working_hours.get_next(now)
|
|
94
|
+
print(f"Next working hour: {next_slot}") # 2025-01-15 11:00:00+00:00
|
|
95
|
+
|
|
96
|
+
# Generate all working hours for a day
|
|
97
|
+
start = datetime(2025, 1, 15, 0, 0, 0, tzinfo=UTC)
|
|
98
|
+
end = datetime(2025, 1, 15, 23, 59, 59, tzinfo=UTC)
|
|
99
|
+
for dt in working_hours.generate(start, end):
|
|
100
|
+
print(dt)
|
|
101
|
+
# Output: 09:00, 10:00, 11:00, 14:00, 15:00, 16:00, 17:00
|
|
102
|
+
|
|
103
|
+
# Serialize for storage
|
|
104
|
+
json_str = working_hours.to_json()
|
|
105
|
+
|
|
106
|
+
# Restore later
|
|
107
|
+
from zerotime import Rule
|
|
108
|
+
restored = Rule.from_json(json_str)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Main APIs
|
|
112
|
+
|
|
113
|
+
### AtomicRule
|
|
114
|
+
|
|
115
|
+
Creates rules from temporal constraints. Each field uses DSL syntax.
|
|
116
|
+
|
|
117
|
+
**Simple usage** - every day at noon:
|
|
118
|
+
```python
|
|
119
|
+
rule = AtomicRule(hours="12", minutes="0", seconds="0")
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**Complex usage** - quarterly reports on the last business day:
|
|
123
|
+
```python
|
|
124
|
+
rule = AtomicRule(
|
|
125
|
+
months="3,6,9,12", # End of quarter
|
|
126
|
+
days="-1,-2,-3", # Last 3 days (will pick based on weekday)
|
|
127
|
+
weekdays="1..5", # Must be a weekday
|
|
128
|
+
hours="17",
|
|
129
|
+
minutes="0",
|
|
130
|
+
seconds="0"
|
|
131
|
+
)
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Rule Operators
|
|
135
|
+
|
|
136
|
+
Combine rules using Python operators:
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
# Union: matches if either rule matches
|
|
140
|
+
holidays = rule1 + rule2
|
|
141
|
+
|
|
142
|
+
# Intersection: matches only if both rules match
|
|
143
|
+
overlap = rule1 & rule2
|
|
144
|
+
|
|
145
|
+
# Difference: matches first rule but not second
|
|
146
|
+
working_days = all_days - holidays
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Temporal Navigation
|
|
150
|
+
|
|
151
|
+
Find next/previous matching datetime:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
# Simple - find next match
|
|
155
|
+
next_match = rule.get_next(datetime.now())
|
|
156
|
+
|
|
157
|
+
# Complex - search up to 10 years ahead
|
|
158
|
+
next_match = rule.get_next(base_date, max_years=10)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Generation
|
|
162
|
+
|
|
163
|
+
Generate all matching datetimes in a range:
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
# Simple - iterate all matches
|
|
167
|
+
for dt in rule.generate(start, end):
|
|
168
|
+
process(dt)
|
|
169
|
+
|
|
170
|
+
# Memory-efficient - process in batches
|
|
171
|
+
for batch in rule.generate_batch(start, end, batch_size=1000):
|
|
172
|
+
bulk_process(batch)
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
### Configuration
|
|
176
|
+
|
|
177
|
+
Adjust global limits:
|
|
178
|
+
|
|
179
|
+
```python
|
|
180
|
+
from zerotime import RuleConfig, set_global_config
|
|
181
|
+
|
|
182
|
+
config = RuleConfig(
|
|
183
|
+
max_years_search=10, # How far to search in get_next/get_prev
|
|
184
|
+
max_generate_items=100000, # Cap on generated items (None = unlimited)
|
|
185
|
+
default_batch_size=5000 # Batch size for generate_batch
|
|
186
|
+
)
|
|
187
|
+
set_global_config(config)
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
## DSL Syntax Reference
|
|
191
|
+
|
|
192
|
+
| Syntax | Meaning | Example |
|
|
193
|
+
|--------|---------|---------|
|
|
194
|
+
| `"N"` | Single value | `"15"` - day 15 |
|
|
195
|
+
| `"N..M"` | Range (inclusive) | `"1..5"` - Mon-Fri |
|
|
196
|
+
| `"N..M/S"` | Range with step | `"0..59/15"` - 0,15,30,45 |
|
|
197
|
+
| `"/S"` | Global step (multiples of S) | `"/15"` - 0,15,30,45 for minutes |
|
|
198
|
+
| `"A,B,C"` | List | `"1,15,-1"` - 1st, 15th, last |
|
|
199
|
+
| `"!N"` | Exclusion | `"1..12,!7,!8"` - all except Jul,Aug |
|
|
200
|
+
| `"-N"` | Negative (days only) | `"-1"` - last day of month |
|
|
201
|
+
|
|
202
|
+
## Installation
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
pip install zerotime
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
**Requirements:**
|
|
209
|
+
- Python 3.11+
|
|
210
|
+
- No external dependencies (standard library only)
|
|
211
|
+
|
|
212
|
+
## Running Tests
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
uv run pytest
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
With coverage:
|
|
219
|
+
|
|
220
|
+
```bash
|
|
221
|
+
uv run pytest --cov=zerotime --cov-report=term-missing
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
## Examples
|
|
225
|
+
|
|
226
|
+
The `examples/` directory contains comprehensive, runnable examples covering all features:
|
|
227
|
+
|
|
228
|
+
| File | Description |
|
|
229
|
+
|------|-------------|
|
|
230
|
+
| `01_basic_usage.py` | Introduction to AtomicRule, get_next, get_prev |
|
|
231
|
+
| `02_dsl_syntax.py` | Complete DSL reference with all syntax patterns |
|
|
232
|
+
| `03_rule_combination.py` | Combining rules with +, &, - operators |
|
|
233
|
+
| `04_generation_methods.py` | generate(), generate_reverse(), generate_batch() |
|
|
234
|
+
| `05_navigation.py` | Temporal navigation and error handling |
|
|
235
|
+
| `06_timezones.py` | Timezone handling including DST transitions |
|
|
236
|
+
| `07_configuration.py` | RuleConfig and global settings |
|
|
237
|
+
| `08_json_serialization.py` | Saving and restoring rules with JSON |
|
|
238
|
+
| `09_builder_methods.py` | Immutable rule modification with with_* methods |
|
|
239
|
+
| `10_real_world_examples.py` | Practical use cases: billing, scheduling, SLA, maintenance windows |
|
|
240
|
+
|
|
241
|
+
Run any example directly:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
python examples/01_basic_usage.py
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
## Further Documentation
|
|
248
|
+
|
|
249
|
+
- [Full API Reference](docs/API_REFERENCE.md)
|
|
250
|
+
- [Functional Analysis](docs/FUNCTIONAL_ANALYSIS.md)
|
|
251
|
+
- [Architecture](docs/ARCHITECTURE.md)
|
|
252
|
+
- [Anti-Patterns](docs/ANTI_PATTERNS.md)
|
|
253
|
+
- [Development](docs/DEVELOPMENT.md)
|
|
254
|
+
- [Examples README](examples/README.md)
|
|
255
|
+
|
|
256
|
+
## License
|
|
257
|
+
|
|
258
|
+
[MIT License](LICENSE) - Copyright (c) 2025 Francesco Favi
|
zerotime-0.1.0/README.md
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
# Zerotime
|
|
2
|
+
|
|
3
|
+
[](https://github.com/francescofavi/zerotime/actions/workflows/ci.yml)
|
|
4
|
+
[](https://pypi.org/project/zerotime/)
|
|
5
|
+
[](https://pypi.org/project/zerotime/)
|
|
6
|
+
[](LICENSE)
|
|
7
|
+
|
|
8
|
+
A Python datetime rule engine for defining and working with recurring time patterns. Zerotime lets you express complex scheduling rules declaratively using a simple DSL (Domain-Specific Language), then query for matching datetimes, generate sequences, or combine rules using set operations.
|
|
9
|
+
|
|
10
|
+
## Why Zerotime?
|
|
11
|
+
|
|
12
|
+
Working with recurring events in datetime is surprisingly complex. You might need "every Monday at 9 AM", "the last day of each month", or "business hours except lunch break". Traditional approaches involve writing custom logic for each pattern, handling edge cases like leap years, varying month lengths, and weekday calculations.
|
|
13
|
+
|
|
14
|
+
Zerotime solves this by providing a unified `Rule` abstraction. Rules can be atomic (based on month, day, hour constraints) or combined using operators (`+` for union, `&` for intersection, `-` for difference). The library handles all the complexity of calendar math internally, provides memory-efficient generation of large date ranges, and includes JSON serialization for persistence.
|
|
15
|
+
|
|
16
|
+
## Technical Design
|
|
17
|
+
|
|
18
|
+
Zerotime uses **only the Python standard library** (no external dependencies). The core design principles:
|
|
19
|
+
|
|
20
|
+
- **Declarative DSL**: Each temporal field (months, days, weekdays, hours, minutes, seconds) accepts string expressions like `"1..5"` (range), `"/15"` (step), `"1,15,-1"` (list with last-day), or `"1..12,!7,!8"` (exclusions)
|
|
21
|
+
- **Composable Rules**: Rules combine via Python operators, enabling complex schedules from simple building blocks
|
|
22
|
+
- **Lazy Generation**: The `generate()` method yields datetimes on demand, suitable for large date ranges
|
|
23
|
+
- **Immutable Rules**: All `with_*` methods return new rule instances; original rules are never modified
|
|
24
|
+
- **Timezone Support**: Rules optionally bind to a timezone, with proper DST handling and validation of timezone-aware vs naive datetimes
|
|
25
|
+
- **Thread Safety**: Parsed expressions are cached with double-checked locking for safe concurrent use
|
|
26
|
+
|
|
27
|
+
## Components Overview
|
|
28
|
+
|
|
29
|
+
The library consists of three main components:
|
|
30
|
+
|
|
31
|
+
- **AtomicRule**: The fundamental building block. Defines temporal constraints using DSL expressions for each datetime field. All constraints must match (AND logic).
|
|
32
|
+
|
|
33
|
+
- **CombinedRule**: Created by combining two rules with an operator. Supports union (either matches), intersection (both match), and difference (first matches but not second).
|
|
34
|
+
|
|
35
|
+
- **RuleConfig**: Global configuration controlling search limits, generation caps, JSON size limits, and batch sizes for memory-efficient processing.
|
|
36
|
+
|
|
37
|
+
## Usage Example
|
|
38
|
+
|
|
39
|
+
Here's a realistic example showing how to define business working hours excluding lunch breaks and holidays:
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from datetime import datetime, UTC
|
|
43
|
+
from zerotime import AtomicRule
|
|
44
|
+
|
|
45
|
+
# Business hours: weekdays 9-17, on the hour
|
|
46
|
+
business_hours = AtomicRule(
|
|
47
|
+
weekdays="1..5", # Monday to Friday
|
|
48
|
+
hours="9..17", # 9 AM to 5 PM
|
|
49
|
+
minutes="0", # On the hour
|
|
50
|
+
seconds="0",
|
|
51
|
+
timezone=UTC
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
# Lunch break: 12-13
|
|
55
|
+
lunch_break = AtomicRule(
|
|
56
|
+
weekdays="1..5",
|
|
57
|
+
hours="12,13",
|
|
58
|
+
minutes="0",
|
|
59
|
+
seconds="0",
|
|
60
|
+
timezone=UTC
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
# Working hours = business hours minus lunch
|
|
64
|
+
working_hours = business_hours - lunch_break
|
|
65
|
+
|
|
66
|
+
# Find the next working hour from now
|
|
67
|
+
now = datetime(2025, 1, 15, 10, 30, 0, tzinfo=UTC)
|
|
68
|
+
next_slot = working_hours.get_next(now)
|
|
69
|
+
print(f"Next working hour: {next_slot}") # 2025-01-15 11:00:00+00:00
|
|
70
|
+
|
|
71
|
+
# Generate all working hours for a day
|
|
72
|
+
start = datetime(2025, 1, 15, 0, 0, 0, tzinfo=UTC)
|
|
73
|
+
end = datetime(2025, 1, 15, 23, 59, 59, tzinfo=UTC)
|
|
74
|
+
for dt in working_hours.generate(start, end):
|
|
75
|
+
print(dt)
|
|
76
|
+
# Output: 09:00, 10:00, 11:00, 14:00, 15:00, 16:00, 17:00
|
|
77
|
+
|
|
78
|
+
# Serialize for storage
|
|
79
|
+
json_str = working_hours.to_json()
|
|
80
|
+
|
|
81
|
+
# Restore later
|
|
82
|
+
from zerotime import Rule
|
|
83
|
+
restored = Rule.from_json(json_str)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## Main APIs
|
|
87
|
+
|
|
88
|
+
### AtomicRule
|
|
89
|
+
|
|
90
|
+
Creates rules from temporal constraints. Each field uses DSL syntax.
|
|
91
|
+
|
|
92
|
+
**Simple usage** - every day at noon:
|
|
93
|
+
```python
|
|
94
|
+
rule = AtomicRule(hours="12", minutes="0", seconds="0")
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Complex usage** - quarterly reports on the last business day:
|
|
98
|
+
```python
|
|
99
|
+
rule = AtomicRule(
|
|
100
|
+
months="3,6,9,12", # End of quarter
|
|
101
|
+
days="-1,-2,-3", # Last 3 days (will pick based on weekday)
|
|
102
|
+
weekdays="1..5", # Must be a weekday
|
|
103
|
+
hours="17",
|
|
104
|
+
minutes="0",
|
|
105
|
+
seconds="0"
|
|
106
|
+
)
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Rule Operators
|
|
110
|
+
|
|
111
|
+
Combine rules using Python operators:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
# Union: matches if either rule matches
|
|
115
|
+
holidays = rule1 + rule2
|
|
116
|
+
|
|
117
|
+
# Intersection: matches only if both rules match
|
|
118
|
+
overlap = rule1 & rule2
|
|
119
|
+
|
|
120
|
+
# Difference: matches first rule but not second
|
|
121
|
+
working_days = all_days - holidays
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Temporal Navigation
|
|
125
|
+
|
|
126
|
+
Find next/previous matching datetime:
|
|
127
|
+
|
|
128
|
+
```python
|
|
129
|
+
# Simple - find next match
|
|
130
|
+
next_match = rule.get_next(datetime.now())
|
|
131
|
+
|
|
132
|
+
# Complex - search up to 10 years ahead
|
|
133
|
+
next_match = rule.get_next(base_date, max_years=10)
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
### Generation
|
|
137
|
+
|
|
138
|
+
Generate all matching datetimes in a range:
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
# Simple - iterate all matches
|
|
142
|
+
for dt in rule.generate(start, end):
|
|
143
|
+
process(dt)
|
|
144
|
+
|
|
145
|
+
# Memory-efficient - process in batches
|
|
146
|
+
for batch in rule.generate_batch(start, end, batch_size=1000):
|
|
147
|
+
bulk_process(batch)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Configuration
|
|
151
|
+
|
|
152
|
+
Adjust global limits:
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
from zerotime import RuleConfig, set_global_config
|
|
156
|
+
|
|
157
|
+
config = RuleConfig(
|
|
158
|
+
max_years_search=10, # How far to search in get_next/get_prev
|
|
159
|
+
max_generate_items=100000, # Cap on generated items (None = unlimited)
|
|
160
|
+
default_batch_size=5000 # Batch size for generate_batch
|
|
161
|
+
)
|
|
162
|
+
set_global_config(config)
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## DSL Syntax Reference
|
|
166
|
+
|
|
167
|
+
| Syntax | Meaning | Example |
|
|
168
|
+
|--------|---------|---------|
|
|
169
|
+
| `"N"` | Single value | `"15"` - day 15 |
|
|
170
|
+
| `"N..M"` | Range (inclusive) | `"1..5"` - Mon-Fri |
|
|
171
|
+
| `"N..M/S"` | Range with step | `"0..59/15"` - 0,15,30,45 |
|
|
172
|
+
| `"/S"` | Global step (multiples of S) | `"/15"` - 0,15,30,45 for minutes |
|
|
173
|
+
| `"A,B,C"` | List | `"1,15,-1"` - 1st, 15th, last |
|
|
174
|
+
| `"!N"` | Exclusion | `"1..12,!7,!8"` - all except Jul,Aug |
|
|
175
|
+
| `"-N"` | Negative (days only) | `"-1"` - last day of month |
|
|
176
|
+
|
|
177
|
+
## Installation
|
|
178
|
+
|
|
179
|
+
```bash
|
|
180
|
+
pip install zerotime
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
**Requirements:**
|
|
184
|
+
- Python 3.11+
|
|
185
|
+
- No external dependencies (standard library only)
|
|
186
|
+
|
|
187
|
+
## Running Tests
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
uv run pytest
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
With coverage:
|
|
194
|
+
|
|
195
|
+
```bash
|
|
196
|
+
uv run pytest --cov=zerotime --cov-report=term-missing
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
## Examples
|
|
200
|
+
|
|
201
|
+
The `examples/` directory contains comprehensive, runnable examples covering all features:
|
|
202
|
+
|
|
203
|
+
| File | Description |
|
|
204
|
+
|------|-------------|
|
|
205
|
+
| `01_basic_usage.py` | Introduction to AtomicRule, get_next, get_prev |
|
|
206
|
+
| `02_dsl_syntax.py` | Complete DSL reference with all syntax patterns |
|
|
207
|
+
| `03_rule_combination.py` | Combining rules with +, &, - operators |
|
|
208
|
+
| `04_generation_methods.py` | generate(), generate_reverse(), generate_batch() |
|
|
209
|
+
| `05_navigation.py` | Temporal navigation and error handling |
|
|
210
|
+
| `06_timezones.py` | Timezone handling including DST transitions |
|
|
211
|
+
| `07_configuration.py` | RuleConfig and global settings |
|
|
212
|
+
| `08_json_serialization.py` | Saving and restoring rules with JSON |
|
|
213
|
+
| `09_builder_methods.py` | Immutable rule modification with with_* methods |
|
|
214
|
+
| `10_real_world_examples.py` | Practical use cases: billing, scheduling, SLA, maintenance windows |
|
|
215
|
+
|
|
216
|
+
Run any example directly:
|
|
217
|
+
|
|
218
|
+
```bash
|
|
219
|
+
python examples/01_basic_usage.py
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Further Documentation
|
|
223
|
+
|
|
224
|
+
- [Full API Reference](docs/API_REFERENCE.md)
|
|
225
|
+
- [Functional Analysis](docs/FUNCTIONAL_ANALYSIS.md)
|
|
226
|
+
- [Architecture](docs/ARCHITECTURE.md)
|
|
227
|
+
- [Anti-Patterns](docs/ANTI_PATTERNS.md)
|
|
228
|
+
- [Development](docs/DEVELOPMENT.md)
|
|
229
|
+
- [Examples README](examples/README.md)
|
|
230
|
+
|
|
231
|
+
## License
|
|
232
|
+
|
|
233
|
+
[MIT License](LICENSE) - Copyright (c) 2025 Francesco Favi
|