fiscalclock 0.1.0__tar.gz → 0.2.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.
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/Cargo.lock +3 -3
- fiscalclock-0.2.0/PKG-INFO +180 -0
- fiscalclock-0.2.0/README.md +147 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/Cargo.toml +2 -2
- fiscalclock-0.2.0/bindings/python/README.md +147 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/src/lib.rs +6 -6
- fiscalclock-0.2.0/bindings/python/tests/test_api.py +335 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/Cargo.toml +2 -1
- fiscalclock-0.2.0/crates/core/README.md +148 -0
- fiscalclock-0.2.0/crates/core/src/calendar.rs +979 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/definition.rs +728 -307
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/dimension.rs +88 -66
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/error.rs +25 -9
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/explain.rs +101 -60
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/json.rs +56 -49
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/label.rs +65 -51
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/lib.rs +6 -2
- fiscalclock-0.2.0/crates/core/src/timeline.rs +678 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/pyproject.toml +2 -2
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/python/fiscalclock/__init__.py +56 -10
- fiscalclock-0.1.0/PKG-INFO +0 -119
- fiscalclock-0.1.0/README.md +0 -86
- fiscalclock-0.1.0/bindings/python/README.md +0 -86
- fiscalclock-0.1.0/bindings/python/tests/test_api.py +0 -145
- fiscalclock-0.1.0/crates/core/README.md +0 -80
- fiscalclock-0.1.0/crates/core/src/calendar.rs +0 -1055
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/Cargo.toml +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/LICENSE-APACHE +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/LICENSE-MIT +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/LICENSE-APACHE +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/LICENSE-MIT +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/tests/test_conformance.py +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/LICENSE-APACHE +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/LICENSE-MIT +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/date.rs +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/strict.rs +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/python/fiscalclock/_fiscalclock.pyi +0 -0
- {fiscalclock-0.1.0 → fiscalclock-0.2.0}/python/fiscalclock/py.typed +0 -0
|
@@ -97,7 +97,7 @@ checksum = "da7c62ceae207dd37ea5b845da6a0696c799f85e97da1ab5b7910be3c1c80223"
|
|
|
97
97
|
|
|
98
98
|
[[package]]
|
|
99
99
|
name = "fiscalclock"
|
|
100
|
-
version = "0.
|
|
100
|
+
version = "0.2.0"
|
|
101
101
|
dependencies = [
|
|
102
102
|
"proptest",
|
|
103
103
|
"serde",
|
|
@@ -107,7 +107,7 @@ dependencies = [
|
|
|
107
107
|
|
|
108
108
|
[[package]]
|
|
109
109
|
name = "fiscalclock-python"
|
|
110
|
-
version = "0.
|
|
110
|
+
version = "0.2.0"
|
|
111
111
|
dependencies = [
|
|
112
112
|
"fiscalclock",
|
|
113
113
|
"pyo3",
|
|
@@ -115,7 +115,7 @@ dependencies = [
|
|
|
115
115
|
|
|
116
116
|
[[package]]
|
|
117
117
|
name = "fiscalclock-wasm"
|
|
118
|
-
version = "0.
|
|
118
|
+
version = "0.2.0"
|
|
119
119
|
dependencies = [
|
|
120
120
|
"fiscalclock",
|
|
121
121
|
"wasm-bindgen",
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fiscalclock
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Classifier: Development Status :: 3 - Alpha
|
|
5
|
+
Classifier: Intended Audience :: Developers
|
|
6
|
+
Classifier: Intended Audience :: Financial and Insurance Industry
|
|
7
|
+
Classifier: Operating System :: OS Independent
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
15
|
+
Classifier: Programming Language :: Python :: Implementation :: CPython
|
|
16
|
+
Classifier: Programming Language :: Python :: Free Threading :: 2 - Beta
|
|
17
|
+
Classifier: Programming Language :: Rust
|
|
18
|
+
Classifier: Topic :: Office/Business :: Financial :: Accounting
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
License-File: LICENSE-MIT
|
|
21
|
+
License-File: LICENSE-APACHE
|
|
22
|
+
Summary: Fiscal calendar engine: compute any fiscal calendar from a JSON definition
|
|
23
|
+
Keywords: fiscal,calendar,4-5-4,retail,accounting
|
|
24
|
+
Author: Karthik Parameshwara
|
|
25
|
+
License-Expression: MIT OR Apache-2.0
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Description-Content-Type: text/markdown; charset=UTF-8; variant=GFM
|
|
28
|
+
Project-URL: Changelog, https://github.com/Karthik-beta/fiscalclock/blob/main/CHANGELOG.md
|
|
29
|
+
Project-URL: Documentation, https://fiscalclock.karthik.nz
|
|
30
|
+
Project-URL: Homepage, https://github.com/Karthik-beta/fiscalclock
|
|
31
|
+
Project-URL: Issues, https://github.com/Karthik-beta/fiscalclock/issues
|
|
32
|
+
|
|
33
|
+
# fiscalclock for Python
|
|
34
|
+
|
|
35
|
+
[](https://pypi.org/project/fiscalclock/)
|
|
36
|
+
[](https://pypi.org/project/fiscalclock/)
|
|
37
|
+
[](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml)
|
|
38
|
+
[](#licence)
|
|
39
|
+
|
|
40
|
+
Python package for [fiscalclock](https://github.com/Karthik-beta/fiscalclock), a fiscal calendar engine. You describe a fiscal calendar as a JSON definition, and the package answers questions about it: which fiscal year, quarter, period and week a date falls in, the bounds of any period, labels such as `Q4 FY2023`, and a day-by-day date dimension.
|
|
41
|
+
|
|
42
|
+
The package has no built-in calendars. Every calendar is a definition you supply, in the format specified in [`spec/definition-v1.md`](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/definition-v1.md).
|
|
43
|
+
|
|
44
|
+
All computation happens in the fiscalclock Rust core, compiled into this package. The Python layer only converts values, so results are identical to the Rust crate and the npm package.
|
|
45
|
+
|
|
46
|
+
Status as of 15 Nov 2026: 0.2.0 adds definitions whose year rule changes, with transition periods (see the [changelog](https://github.com/Karthik-beta/fiscalclock/blob/main/CHANGELOG.md)). The definition format and API are version 1 drafts and may change before 1.0.
|
|
47
|
+
|
|
48
|
+
## Install
|
|
49
|
+
|
|
50
|
+
```sh
|
|
51
|
+
pip install fiscalclock
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
As of 4 Nov 2026, the [0.1.0 files on PyPI](https://pypi.org/project/fiscalclock/0.1.0/#files) include wheels for CPython 3.10 and later (one stable-ABI `cp310-abi3` wheel per platform) and for free-threaded CPython 3.14 (`cp314t`).
|
|
55
|
+
|
|
56
|
+
## Example
|
|
57
|
+
|
|
58
|
+
A 4-5-4 calendar whose year ends on the Saturday nearest 31 January:
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import datetime as dt
|
|
62
|
+
from fiscalclock import FiscalCalendar
|
|
63
|
+
|
|
64
|
+
definition = {
|
|
65
|
+
"spec_version": 1,
|
|
66
|
+
"year": {"type": "week_based",
|
|
67
|
+
"anchor": {"edge": "end", "month": 1, "day": "last", "rule": "nearest"}},
|
|
68
|
+
"week": {"starts_on": "sunday", "numbering": "contains_start"},
|
|
69
|
+
"subdivision": {"type": "week_pattern", "periods": [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4],
|
|
70
|
+
"quarters": [3, 3, 3, 3], "extra_week": {"period": 12}},
|
|
71
|
+
"naming": {"default": "fy", "schemes": {"fy": {"year_number": "start",
|
|
72
|
+
"templates": {"year": "FY{yyyy}", "quarter": "Q{q} FY{yyyy}"}}}},
|
|
73
|
+
}
|
|
74
|
+
cal = FiscalCalendar(definition)
|
|
75
|
+
|
|
76
|
+
fy2023 = cal.period("year", 2023)
|
|
77
|
+
assert fy2023.start == dt.date(2023, 1, 29)
|
|
78
|
+
assert fy2023.last_day == dt.date(2024, 2, 3)
|
|
79
|
+
assert fy2023.weeks == 53
|
|
80
|
+
|
|
81
|
+
here = cal.locate(dt.date(2024, 2, 3))
|
|
82
|
+
assert cal.format(here.quarter) == "Q4 FY2023"
|
|
83
|
+
assert cal.parse("Q4 FY2023") == here.quarter
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
## API
|
|
87
|
+
|
|
88
|
+
`FiscalCalendar(definition)` takes a JSON string or a mapping, validates it once, and raises `FiscalclockError` if it is invalid. The operations are specified in [`spec/api-v1.md`](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/api-v1.md).
|
|
89
|
+
|
|
90
|
+
| Member | Returns |
|
|
91
|
+
| --- | --- |
|
|
92
|
+
| `locate(date)` | The `Location` of `date`: its year, quarter, period and week. |
|
|
93
|
+
| `period(level, year, index=None)` | The `Period` with those coordinates. Omit `index` for a year. |
|
|
94
|
+
| `shift(period, n)` | The `Period` `n` steps away at the same level. |
|
|
95
|
+
| `parent(period)` | The enclosing `Period` one level up. |
|
|
96
|
+
| `children(period, level)` | A `list[Period]` of the periods at `level` inside `period`. |
|
|
97
|
+
| `years(first, last)` | A `list[Period]` of the years numbered `first` to `last`, inclusive. |
|
|
98
|
+
| `format(period, scheme=None)` | The label, a `str`, under `scheme` or the default scheme. |
|
|
99
|
+
| `parse(label)` | The `Period` that `label` names. |
|
|
100
|
+
| `dimension(start, end_exclusive)` | A `list[dict]`, one row per day, with dates as ISO strings, ready for a data frame. |
|
|
101
|
+
| `dimension_csv(start, end_exclusive)` | The same rows as CSV text. |
|
|
102
|
+
| `explain(date)` | A `dict` with the rules applied to locate `date`, step by step. |
|
|
103
|
+
| `call_json(op, args)` | The core's JSON text for operation `op` with the JSON text `args`. |
|
|
104
|
+
| `calendar_id` | A `str`: the first 16 hex digits of the SHA-256 of `canonical_json`. |
|
|
105
|
+
| `canonical_json` | A `str`: the canonical form of the definition. |
|
|
106
|
+
|
|
107
|
+
- `level` is `"year"`, `"quarter"`, `"period"` or `"week"`.
|
|
108
|
+
- `date`, `start` and `end_exclusive` are a `datetime.date` or an ISO `YYYY-MM-DD` string (see [Dates](#dates)).
|
|
109
|
+
- `period` is a `Period`, or a mapping with its `level`, `year` and `index`.
|
|
110
|
+
- `Period` and `Location` are frozen dataclasses with `datetime.date` fields. Every period is the half-open interval `[start, end_exclusive)`, and `last_day` is the day before `end_exclusive`.
|
|
111
|
+
|
|
112
|
+
The module also has three functions that take a definition on each call:
|
|
113
|
+
|
|
114
|
+
- `validate(definition)` takes a JSON string or a mapping and returns the validation result as a dict. It does not raise.
|
|
115
|
+
- `validate_json(definition)` takes definition text and returns the core's JSON text.
|
|
116
|
+
- `call_json(definition, op, args)` takes definition text, an operation name and argument text, and returns the core's JSON text.
|
|
117
|
+
|
|
118
|
+
The JSON text from `call_json` and `validate_json` is exactly what the core produces, byte for byte the same as in Rust and JavaScript:
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
import json
|
|
122
|
+
from fiscalclock import call_json
|
|
123
|
+
|
|
124
|
+
args = '{"period":{"level":"quarter","year":2023,"index":4}}'
|
|
125
|
+
print(cal.call_json("format", args))
|
|
126
|
+
print(call_json(json.dumps(definition), "format", args))
|
|
127
|
+
# {"label":"Q4 FY2023","scheme":"fy"}
|
|
128
|
+
# {"label":"Q4 FY2023","scheme":"fy"}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
## Errors
|
|
132
|
+
|
|
133
|
+
`FiscalclockError` is a `ValueError`. Its `code`, `path`, `message`, `hint` and `details` describe the first error, and `errors` holds every error as a dict, because validation can report several. `message` is the core's message; `str(error)` combines the code, path, message and hint. Match on `code` and `path`, not on the wording of `message` or `hint`. The codes are listed in the [error catalogue](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/api-v1.md#error-catalogue).
|
|
134
|
+
|
|
135
|
+
When a label matches more than one period, `parse` raises `AMBIGUOUS_LABEL` with every candidate in `details["candidates"]`, and never picks one:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from fiscalclock import FiscalclockError
|
|
139
|
+
|
|
140
|
+
two_schemes = FiscalCalendar({**definition, "naming": {"default": "by_start", "schemes": {
|
|
141
|
+
"by_start": {"year_number": "start", "templates": {"year": "FY{yyyy}"}},
|
|
142
|
+
"by_end": {"year_number": "end", "templates": {"year": "FY{yyyy}"}},
|
|
143
|
+
}}})
|
|
144
|
+
|
|
145
|
+
try:
|
|
146
|
+
two_schemes.parse("FY2024")
|
|
147
|
+
except FiscalclockError as error:
|
|
148
|
+
print(error.code, error.path)
|
|
149
|
+
for candidate in error.details["candidates"]:
|
|
150
|
+
print(candidate["scheme"], candidate["period"]["start"])
|
|
151
|
+
# AMBIGUOUS_LABEL label
|
|
152
|
+
# by_end 2023-01-29
|
|
153
|
+
# by_start 2024-02-04
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
## Dates
|
|
157
|
+
|
|
158
|
+
Date arguments accept only an object whose type is exactly `datetime.date`, or an ISO `YYYY-MM-DD` string. A `datetime.datetime`, including a pandas `Timestamp`, raises `FiscalclockError` with code `TIMESTAMP_REJECTED`, because the calendar day of a timestamp depends on the time zone. Convert it to a date in the time zone you mean first.
|
|
159
|
+
|
|
160
|
+
## How the results are checked
|
|
161
|
+
|
|
162
|
+
- Shared conformance vectors are run by the Rust, Python and npm test suites. Expected results come from cited sources or from independent generators, never from fiscalclock itself. See [`conformance/`](https://github.com/Karthik-beta/fiscalclock/tree/main/conformance).
|
|
163
|
+
- Week-based years are checked against Python's `date.isocalendar()` for every year from 1600 to 2399 ([`oracle_vectors.py`](https://github.com/Karthik-beta/fiscalclock/blob/main/conformance/generators/oracle_vectors.py)), and a slow day-by-day reference implementation is compared with the core for every date from 1900 to 2200 ([`differential.rs`](https://github.com/Karthik-beta/fiscalclock/blob/main/crates/core/tests/differential.rs)).
|
|
164
|
+
- [Property tests](https://github.com/Karthik-beta/fiscalclock/blob/main/crates/core/tests/properties.rs) check that periods tile and nest, and that `shift` and `format`/`parse` round-trip.
|
|
165
|
+
- On every push to `main`, and on every pull request that changes a file the package jobs read, [CI](https://github.com/Karthik-beta/fiscalclock/blob/main/.github/workflows/ci.yml) runs all vectors through Rust on Linux and through the Python and npm packages on Linux, macOS and Windows, and requires the 14 raw outputs to match the Rust output byte for byte.
|
|
166
|
+
|
|
167
|
+
## Supply chain
|
|
168
|
+
|
|
169
|
+
This package is built and published by the [release workflow](https://github.com/Karthik-beta/fiscalclock/blob/main/.github/workflows/release.yml) from a version tag on `main`, after a smoke test of the built packages on Linux, macOS and Windows and the maintainer's approval. Every release file has a [build provenance attestation](https://github.com/Karthik-beta/fiscalclock/attestations). To check a wheel or sdist downloaded from PyPI or the GitHub release:
|
|
170
|
+
|
|
171
|
+
```sh
|
|
172
|
+
gh attestation verify <file> --repo Karthik-beta/fiscalclock
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
Report vulnerabilities privately as described in [SECURITY.md](https://github.com/Karthik-beta/fiscalclock/blob/main/SECURITY.md). Changes are listed in the [changelog](https://github.com/Karthik-beta/fiscalclock/blob/main/CHANGELOG.md).
|
|
176
|
+
|
|
177
|
+
## Licence
|
|
178
|
+
|
|
179
|
+
Licensed under either of [Apache License, Version 2.0](https://github.com/Karthik-beta/fiscalclock/blob/main/LICENSE-APACHE) or [MIT licence](https://github.com/Karthik-beta/fiscalclock/blob/main/LICENSE-MIT), at your option.
|
|
180
|
+
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# fiscalclock for Python
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fiscalclock/)
|
|
4
|
+
[](https://pypi.org/project/fiscalclock/)
|
|
5
|
+
[](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml)
|
|
6
|
+
[](#licence)
|
|
7
|
+
|
|
8
|
+
Python package for [fiscalclock](https://github.com/Karthik-beta/fiscalclock), a fiscal calendar engine. You describe a fiscal calendar as a JSON definition, and the package answers questions about it: which fiscal year, quarter, period and week a date falls in, the bounds of any period, labels such as `Q4 FY2023`, and a day-by-day date dimension.
|
|
9
|
+
|
|
10
|
+
The package has no built-in calendars. Every calendar is a definition you supply, in the format specified in [`spec/definition-v1.md`](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/definition-v1.md).
|
|
11
|
+
|
|
12
|
+
All computation happens in the fiscalclock Rust core, compiled into this package. The Python layer only converts values, so results are identical to the Rust crate and the npm package.
|
|
13
|
+
|
|
14
|
+
Status as of 15 Nov 2026: 0.2.0 adds definitions whose year rule changes, with transition periods (see the [changelog](https://github.com/Karthik-beta/fiscalclock/blob/main/CHANGELOG.md)). The definition format and API are version 1 drafts and may change before 1.0.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install fiscalclock
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
As of 4 Nov 2026, the [0.1.0 files on PyPI](https://pypi.org/project/fiscalclock/0.1.0/#files) include wheels for CPython 3.10 and later (one stable-ABI `cp310-abi3` wheel per platform) and for free-threaded CPython 3.14 (`cp314t`).
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
A 4-5-4 calendar whose year ends on the Saturday nearest 31 January:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import datetime as dt
|
|
30
|
+
from fiscalclock import FiscalCalendar
|
|
31
|
+
|
|
32
|
+
definition = {
|
|
33
|
+
"spec_version": 1,
|
|
34
|
+
"year": {"type": "week_based",
|
|
35
|
+
"anchor": {"edge": "end", "month": 1, "day": "last", "rule": "nearest"}},
|
|
36
|
+
"week": {"starts_on": "sunday", "numbering": "contains_start"},
|
|
37
|
+
"subdivision": {"type": "week_pattern", "periods": [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4],
|
|
38
|
+
"quarters": [3, 3, 3, 3], "extra_week": {"period": 12}},
|
|
39
|
+
"naming": {"default": "fy", "schemes": {"fy": {"year_number": "start",
|
|
40
|
+
"templates": {"year": "FY{yyyy}", "quarter": "Q{q} FY{yyyy}"}}}},
|
|
41
|
+
}
|
|
42
|
+
cal = FiscalCalendar(definition)
|
|
43
|
+
|
|
44
|
+
fy2023 = cal.period("year", 2023)
|
|
45
|
+
assert fy2023.start == dt.date(2023, 1, 29)
|
|
46
|
+
assert fy2023.last_day == dt.date(2024, 2, 3)
|
|
47
|
+
assert fy2023.weeks == 53
|
|
48
|
+
|
|
49
|
+
here = cal.locate(dt.date(2024, 2, 3))
|
|
50
|
+
assert cal.format(here.quarter) == "Q4 FY2023"
|
|
51
|
+
assert cal.parse("Q4 FY2023") == here.quarter
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API
|
|
55
|
+
|
|
56
|
+
`FiscalCalendar(definition)` takes a JSON string or a mapping, validates it once, and raises `FiscalclockError` if it is invalid. The operations are specified in [`spec/api-v1.md`](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/api-v1.md).
|
|
57
|
+
|
|
58
|
+
| Member | Returns |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `locate(date)` | The `Location` of `date`: its year, quarter, period and week. |
|
|
61
|
+
| `period(level, year, index=None)` | The `Period` with those coordinates. Omit `index` for a year. |
|
|
62
|
+
| `shift(period, n)` | The `Period` `n` steps away at the same level. |
|
|
63
|
+
| `parent(period)` | The enclosing `Period` one level up. |
|
|
64
|
+
| `children(period, level)` | A `list[Period]` of the periods at `level` inside `period`. |
|
|
65
|
+
| `years(first, last)` | A `list[Period]` of the years numbered `first` to `last`, inclusive. |
|
|
66
|
+
| `format(period, scheme=None)` | The label, a `str`, under `scheme` or the default scheme. |
|
|
67
|
+
| `parse(label)` | The `Period` that `label` names. |
|
|
68
|
+
| `dimension(start, end_exclusive)` | A `list[dict]`, one row per day, with dates as ISO strings, ready for a data frame. |
|
|
69
|
+
| `dimension_csv(start, end_exclusive)` | The same rows as CSV text. |
|
|
70
|
+
| `explain(date)` | A `dict` with the rules applied to locate `date`, step by step. |
|
|
71
|
+
| `call_json(op, args)` | The core's JSON text for operation `op` with the JSON text `args`. |
|
|
72
|
+
| `calendar_id` | A `str`: the first 16 hex digits of the SHA-256 of `canonical_json`. |
|
|
73
|
+
| `canonical_json` | A `str`: the canonical form of the definition. |
|
|
74
|
+
|
|
75
|
+
- `level` is `"year"`, `"quarter"`, `"period"` or `"week"`.
|
|
76
|
+
- `date`, `start` and `end_exclusive` are a `datetime.date` or an ISO `YYYY-MM-DD` string (see [Dates](#dates)).
|
|
77
|
+
- `period` is a `Period`, or a mapping with its `level`, `year` and `index`.
|
|
78
|
+
- `Period` and `Location` are frozen dataclasses with `datetime.date` fields. Every period is the half-open interval `[start, end_exclusive)`, and `last_day` is the day before `end_exclusive`.
|
|
79
|
+
|
|
80
|
+
The module also has three functions that take a definition on each call:
|
|
81
|
+
|
|
82
|
+
- `validate(definition)` takes a JSON string or a mapping and returns the validation result as a dict. It does not raise.
|
|
83
|
+
- `validate_json(definition)` takes definition text and returns the core's JSON text.
|
|
84
|
+
- `call_json(definition, op, args)` takes definition text, an operation name and argument text, and returns the core's JSON text.
|
|
85
|
+
|
|
86
|
+
The JSON text from `call_json` and `validate_json` is exactly what the core produces, byte for byte the same as in Rust and JavaScript:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import json
|
|
90
|
+
from fiscalclock import call_json
|
|
91
|
+
|
|
92
|
+
args = '{"period":{"level":"quarter","year":2023,"index":4}}'
|
|
93
|
+
print(cal.call_json("format", args))
|
|
94
|
+
print(call_json(json.dumps(definition), "format", args))
|
|
95
|
+
# {"label":"Q4 FY2023","scheme":"fy"}
|
|
96
|
+
# {"label":"Q4 FY2023","scheme":"fy"}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Errors
|
|
100
|
+
|
|
101
|
+
`FiscalclockError` is a `ValueError`. Its `code`, `path`, `message`, `hint` and `details` describe the first error, and `errors` holds every error as a dict, because validation can report several. `message` is the core's message; `str(error)` combines the code, path, message and hint. Match on `code` and `path`, not on the wording of `message` or `hint`. The codes are listed in the [error catalogue](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/api-v1.md#error-catalogue).
|
|
102
|
+
|
|
103
|
+
When a label matches more than one period, `parse` raises `AMBIGUOUS_LABEL` with every candidate in `details["candidates"]`, and never picks one:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from fiscalclock import FiscalclockError
|
|
107
|
+
|
|
108
|
+
two_schemes = FiscalCalendar({**definition, "naming": {"default": "by_start", "schemes": {
|
|
109
|
+
"by_start": {"year_number": "start", "templates": {"year": "FY{yyyy}"}},
|
|
110
|
+
"by_end": {"year_number": "end", "templates": {"year": "FY{yyyy}"}},
|
|
111
|
+
}}})
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
two_schemes.parse("FY2024")
|
|
115
|
+
except FiscalclockError as error:
|
|
116
|
+
print(error.code, error.path)
|
|
117
|
+
for candidate in error.details["candidates"]:
|
|
118
|
+
print(candidate["scheme"], candidate["period"]["start"])
|
|
119
|
+
# AMBIGUOUS_LABEL label
|
|
120
|
+
# by_end 2023-01-29
|
|
121
|
+
# by_start 2024-02-04
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Dates
|
|
125
|
+
|
|
126
|
+
Date arguments accept only an object whose type is exactly `datetime.date`, or an ISO `YYYY-MM-DD` string. A `datetime.datetime`, including a pandas `Timestamp`, raises `FiscalclockError` with code `TIMESTAMP_REJECTED`, because the calendar day of a timestamp depends on the time zone. Convert it to a date in the time zone you mean first.
|
|
127
|
+
|
|
128
|
+
## How the results are checked
|
|
129
|
+
|
|
130
|
+
- Shared conformance vectors are run by the Rust, Python and npm test suites. Expected results come from cited sources or from independent generators, never from fiscalclock itself. See [`conformance/`](https://github.com/Karthik-beta/fiscalclock/tree/main/conformance).
|
|
131
|
+
- Week-based years are checked against Python's `date.isocalendar()` for every year from 1600 to 2399 ([`oracle_vectors.py`](https://github.com/Karthik-beta/fiscalclock/blob/main/conformance/generators/oracle_vectors.py)), and a slow day-by-day reference implementation is compared with the core for every date from 1900 to 2200 ([`differential.rs`](https://github.com/Karthik-beta/fiscalclock/blob/main/crates/core/tests/differential.rs)).
|
|
132
|
+
- [Property tests](https://github.com/Karthik-beta/fiscalclock/blob/main/crates/core/tests/properties.rs) check that periods tile and nest, and that `shift` and `format`/`parse` round-trip.
|
|
133
|
+
- On every push to `main`, and on every pull request that changes a file the package jobs read, [CI](https://github.com/Karthik-beta/fiscalclock/blob/main/.github/workflows/ci.yml) runs all vectors through Rust on Linux and through the Python and npm packages on Linux, macOS and Windows, and requires the 14 raw outputs to match the Rust output byte for byte.
|
|
134
|
+
|
|
135
|
+
## Supply chain
|
|
136
|
+
|
|
137
|
+
This package is built and published by the [release workflow](https://github.com/Karthik-beta/fiscalclock/blob/main/.github/workflows/release.yml) from a version tag on `main`, after a smoke test of the built packages on Linux, macOS and Windows and the maintainer's approval. Every release file has a [build provenance attestation](https://github.com/Karthik-beta/fiscalclock/attestations). To check a wheel or sdist downloaded from PyPI or the GitHub release:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
gh attestation verify <file> --repo Karthik-beta/fiscalclock
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Report vulnerabilities privately as described in [SECURITY.md](https://github.com/Karthik-beta/fiscalclock/blob/main/SECURITY.md). Changes are listed in the [changelog](https://github.com/Karthik-beta/fiscalclock/blob/main/CHANGELOG.md).
|
|
144
|
+
|
|
145
|
+
## Licence
|
|
146
|
+
|
|
147
|
+
Licensed under either of [Apache License, Version 2.0](https://github.com/Karthik-beta/fiscalclock/blob/main/LICENSE-APACHE) or [MIT licence](https://github.com/Karthik-beta/fiscalclock/blob/main/LICENSE-MIT), at your option.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[package]
|
|
2
2
|
name = "fiscalclock-python"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
edition = "2024"
|
|
5
5
|
rust-version = "1.85"
|
|
6
6
|
license = "MIT OR Apache-2.0"
|
|
@@ -14,7 +14,7 @@ name = "_fiscalclock"
|
|
|
14
14
|
crate-type = ["cdylib"]
|
|
15
15
|
|
|
16
16
|
[dependencies]
|
|
17
|
-
fiscalclock = { path = "../../crates/core", version = "=0.
|
|
17
|
+
fiscalclock = { path = "../../crates/core", version = "=0.2.0" }
|
|
18
18
|
pyo3 = "0.30"
|
|
19
19
|
|
|
20
20
|
[features]
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
# fiscalclock for Python
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/fiscalclock/)
|
|
4
|
+
[](https://pypi.org/project/fiscalclock/)
|
|
5
|
+
[](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml)
|
|
6
|
+
[](#licence)
|
|
7
|
+
|
|
8
|
+
Python package for [fiscalclock](https://github.com/Karthik-beta/fiscalclock), a fiscal calendar engine. You describe a fiscal calendar as a JSON definition, and the package answers questions about it: which fiscal year, quarter, period and week a date falls in, the bounds of any period, labels such as `Q4 FY2023`, and a day-by-day date dimension.
|
|
9
|
+
|
|
10
|
+
The package has no built-in calendars. Every calendar is a definition you supply, in the format specified in [`spec/definition-v1.md`](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/definition-v1.md).
|
|
11
|
+
|
|
12
|
+
All computation happens in the fiscalclock Rust core, compiled into this package. The Python layer only converts values, so results are identical to the Rust crate and the npm package.
|
|
13
|
+
|
|
14
|
+
Status as of 15 Nov 2026: 0.2.0 adds definitions whose year rule changes, with transition periods (see the [changelog](https://github.com/Karthik-beta/fiscalclock/blob/main/CHANGELOG.md)). The definition format and API are version 1 drafts and may change before 1.0.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```sh
|
|
19
|
+
pip install fiscalclock
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
As of 4 Nov 2026, the [0.1.0 files on PyPI](https://pypi.org/project/fiscalclock/0.1.0/#files) include wheels for CPython 3.10 and later (one stable-ABI `cp310-abi3` wheel per platform) and for free-threaded CPython 3.14 (`cp314t`).
|
|
23
|
+
|
|
24
|
+
## Example
|
|
25
|
+
|
|
26
|
+
A 4-5-4 calendar whose year ends on the Saturday nearest 31 January:
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
import datetime as dt
|
|
30
|
+
from fiscalclock import FiscalCalendar
|
|
31
|
+
|
|
32
|
+
definition = {
|
|
33
|
+
"spec_version": 1,
|
|
34
|
+
"year": {"type": "week_based",
|
|
35
|
+
"anchor": {"edge": "end", "month": 1, "day": "last", "rule": "nearest"}},
|
|
36
|
+
"week": {"starts_on": "sunday", "numbering": "contains_start"},
|
|
37
|
+
"subdivision": {"type": "week_pattern", "periods": [4, 5, 4, 4, 5, 4, 4, 5, 4, 4, 5, 4],
|
|
38
|
+
"quarters": [3, 3, 3, 3], "extra_week": {"period": 12}},
|
|
39
|
+
"naming": {"default": "fy", "schemes": {"fy": {"year_number": "start",
|
|
40
|
+
"templates": {"year": "FY{yyyy}", "quarter": "Q{q} FY{yyyy}"}}}},
|
|
41
|
+
}
|
|
42
|
+
cal = FiscalCalendar(definition)
|
|
43
|
+
|
|
44
|
+
fy2023 = cal.period("year", 2023)
|
|
45
|
+
assert fy2023.start == dt.date(2023, 1, 29)
|
|
46
|
+
assert fy2023.last_day == dt.date(2024, 2, 3)
|
|
47
|
+
assert fy2023.weeks == 53
|
|
48
|
+
|
|
49
|
+
here = cal.locate(dt.date(2024, 2, 3))
|
|
50
|
+
assert cal.format(here.quarter) == "Q4 FY2023"
|
|
51
|
+
assert cal.parse("Q4 FY2023") == here.quarter
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## API
|
|
55
|
+
|
|
56
|
+
`FiscalCalendar(definition)` takes a JSON string or a mapping, validates it once, and raises `FiscalclockError` if it is invalid. The operations are specified in [`spec/api-v1.md`](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/api-v1.md).
|
|
57
|
+
|
|
58
|
+
| Member | Returns |
|
|
59
|
+
| --- | --- |
|
|
60
|
+
| `locate(date)` | The `Location` of `date`: its year, quarter, period and week. |
|
|
61
|
+
| `period(level, year, index=None)` | The `Period` with those coordinates. Omit `index` for a year. |
|
|
62
|
+
| `shift(period, n)` | The `Period` `n` steps away at the same level. |
|
|
63
|
+
| `parent(period)` | The enclosing `Period` one level up. |
|
|
64
|
+
| `children(period, level)` | A `list[Period]` of the periods at `level` inside `period`. |
|
|
65
|
+
| `years(first, last)` | A `list[Period]` of the years numbered `first` to `last`, inclusive. |
|
|
66
|
+
| `format(period, scheme=None)` | The label, a `str`, under `scheme` or the default scheme. |
|
|
67
|
+
| `parse(label)` | The `Period` that `label` names. |
|
|
68
|
+
| `dimension(start, end_exclusive)` | A `list[dict]`, one row per day, with dates as ISO strings, ready for a data frame. |
|
|
69
|
+
| `dimension_csv(start, end_exclusive)` | The same rows as CSV text. |
|
|
70
|
+
| `explain(date)` | A `dict` with the rules applied to locate `date`, step by step. |
|
|
71
|
+
| `call_json(op, args)` | The core's JSON text for operation `op` with the JSON text `args`. |
|
|
72
|
+
| `calendar_id` | A `str`: the first 16 hex digits of the SHA-256 of `canonical_json`. |
|
|
73
|
+
| `canonical_json` | A `str`: the canonical form of the definition. |
|
|
74
|
+
|
|
75
|
+
- `level` is `"year"`, `"quarter"`, `"period"` or `"week"`.
|
|
76
|
+
- `date`, `start` and `end_exclusive` are a `datetime.date` or an ISO `YYYY-MM-DD` string (see [Dates](#dates)).
|
|
77
|
+
- `period` is a `Period`, or a mapping with its `level`, `year` and `index`.
|
|
78
|
+
- `Period` and `Location` are frozen dataclasses with `datetime.date` fields. Every period is the half-open interval `[start, end_exclusive)`, and `last_day` is the day before `end_exclusive`.
|
|
79
|
+
|
|
80
|
+
The module also has three functions that take a definition on each call:
|
|
81
|
+
|
|
82
|
+
- `validate(definition)` takes a JSON string or a mapping and returns the validation result as a dict. It does not raise.
|
|
83
|
+
- `validate_json(definition)` takes definition text and returns the core's JSON text.
|
|
84
|
+
- `call_json(definition, op, args)` takes definition text, an operation name and argument text, and returns the core's JSON text.
|
|
85
|
+
|
|
86
|
+
The JSON text from `call_json` and `validate_json` is exactly what the core produces, byte for byte the same as in Rust and JavaScript:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
import json
|
|
90
|
+
from fiscalclock import call_json
|
|
91
|
+
|
|
92
|
+
args = '{"period":{"level":"quarter","year":2023,"index":4}}'
|
|
93
|
+
print(cal.call_json("format", args))
|
|
94
|
+
print(call_json(json.dumps(definition), "format", args))
|
|
95
|
+
# {"label":"Q4 FY2023","scheme":"fy"}
|
|
96
|
+
# {"label":"Q4 FY2023","scheme":"fy"}
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Errors
|
|
100
|
+
|
|
101
|
+
`FiscalclockError` is a `ValueError`. Its `code`, `path`, `message`, `hint` and `details` describe the first error, and `errors` holds every error as a dict, because validation can report several. `message` is the core's message; `str(error)` combines the code, path, message and hint. Match on `code` and `path`, not on the wording of `message` or `hint`. The codes are listed in the [error catalogue](https://github.com/Karthik-beta/fiscalclock/blob/main/spec/api-v1.md#error-catalogue).
|
|
102
|
+
|
|
103
|
+
When a label matches more than one period, `parse` raises `AMBIGUOUS_LABEL` with every candidate in `details["candidates"]`, and never picks one:
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from fiscalclock import FiscalclockError
|
|
107
|
+
|
|
108
|
+
two_schemes = FiscalCalendar({**definition, "naming": {"default": "by_start", "schemes": {
|
|
109
|
+
"by_start": {"year_number": "start", "templates": {"year": "FY{yyyy}"}},
|
|
110
|
+
"by_end": {"year_number": "end", "templates": {"year": "FY{yyyy}"}},
|
|
111
|
+
}}})
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
two_schemes.parse("FY2024")
|
|
115
|
+
except FiscalclockError as error:
|
|
116
|
+
print(error.code, error.path)
|
|
117
|
+
for candidate in error.details["candidates"]:
|
|
118
|
+
print(candidate["scheme"], candidate["period"]["start"])
|
|
119
|
+
# AMBIGUOUS_LABEL label
|
|
120
|
+
# by_end 2023-01-29
|
|
121
|
+
# by_start 2024-02-04
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## Dates
|
|
125
|
+
|
|
126
|
+
Date arguments accept only an object whose type is exactly `datetime.date`, or an ISO `YYYY-MM-DD` string. A `datetime.datetime`, including a pandas `Timestamp`, raises `FiscalclockError` with code `TIMESTAMP_REJECTED`, because the calendar day of a timestamp depends on the time zone. Convert it to a date in the time zone you mean first.
|
|
127
|
+
|
|
128
|
+
## How the results are checked
|
|
129
|
+
|
|
130
|
+
- Shared conformance vectors are run by the Rust, Python and npm test suites. Expected results come from cited sources or from independent generators, never from fiscalclock itself. See [`conformance/`](https://github.com/Karthik-beta/fiscalclock/tree/main/conformance).
|
|
131
|
+
- Week-based years are checked against Python's `date.isocalendar()` for every year from 1600 to 2399 ([`oracle_vectors.py`](https://github.com/Karthik-beta/fiscalclock/blob/main/conformance/generators/oracle_vectors.py)), and a slow day-by-day reference implementation is compared with the core for every date from 1900 to 2200 ([`differential.rs`](https://github.com/Karthik-beta/fiscalclock/blob/main/crates/core/tests/differential.rs)).
|
|
132
|
+
- [Property tests](https://github.com/Karthik-beta/fiscalclock/blob/main/crates/core/tests/properties.rs) check that periods tile and nest, and that `shift` and `format`/`parse` round-trip.
|
|
133
|
+
- On every push to `main`, and on every pull request that changes a file the package jobs read, [CI](https://github.com/Karthik-beta/fiscalclock/blob/main/.github/workflows/ci.yml) runs all vectors through Rust on Linux and through the Python and npm packages on Linux, macOS and Windows, and requires the 14 raw outputs to match the Rust output byte for byte.
|
|
134
|
+
|
|
135
|
+
## Supply chain
|
|
136
|
+
|
|
137
|
+
This package is built and published by the [release workflow](https://github.com/Karthik-beta/fiscalclock/blob/main/.github/workflows/release.yml) from a version tag on `main`, after a smoke test of the built packages on Linux, macOS and Windows and the maintainer's approval. Every release file has a [build provenance attestation](https://github.com/Karthik-beta/fiscalclock/attestations). To check a wheel or sdist downloaded from PyPI or the GitHub release:
|
|
138
|
+
|
|
139
|
+
```sh
|
|
140
|
+
gh attestation verify <file> --repo Karthik-beta/fiscalclock
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Report vulnerabilities privately as described in [SECURITY.md](https://github.com/Karthik-beta/fiscalclock/blob/main/SECURITY.md). Changes are listed in the [changelog](https://github.com/Karthik-beta/fiscalclock/blob/main/CHANGELOG.md).
|
|
144
|
+
|
|
145
|
+
## Licence
|
|
146
|
+
|
|
147
|
+
Licensed under either of [Apache License, Version 2.0](https://github.com/Karthik-beta/fiscalclock/blob/main/LICENSE-APACHE) or [MIT licence](https://github.com/Karthik-beta/fiscalclock/blob/main/LICENSE-MIT), at your option.
|
|
@@ -12,14 +12,14 @@ use pyo3::prelude::*;
|
|
|
12
12
|
|
|
13
13
|
/// `fiscalclock::json::validate`.
|
|
14
14
|
#[pyfunction]
|
|
15
|
-
fn validate(definition: &str) -> String {
|
|
16
|
-
json::validate(definition)
|
|
15
|
+
fn validate(py: Python<'_>, definition: &str) -> String {
|
|
16
|
+
py.detach(|| json::validate(definition))
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/// `fiscalclock::json::call`.
|
|
20
20
|
#[pyfunction]
|
|
21
|
-
fn call(definition: &str, op: &str, args: &str) -> String {
|
|
22
|
-
json::call(definition, op, args)
|
|
21
|
+
fn call(py: Python<'_>, definition: &str, op: &str, args: &str) -> String {
|
|
22
|
+
py.detach(|| json::call(definition, op, args))
|
|
23
23
|
}
|
|
24
24
|
|
|
25
25
|
/// `fiscalclock::json::timestamp_rejected`.
|
|
@@ -36,8 +36,8 @@ struct Calendar(FiscalCalendar);
|
|
|
36
36
|
impl Calendar {
|
|
37
37
|
/// Returns `(calendar, None)`, or `(None, errors)` with the core's errors JSON.
|
|
38
38
|
#[staticmethod]
|
|
39
|
-
fn build(definition: &str) -> (Option<Calendar>, Option<String>) {
|
|
40
|
-
match FiscalCalendar::from_json(definition) {
|
|
39
|
+
fn build(py: Python<'_>, definition: &str) -> (Option<Calendar>, Option<String>) {
|
|
40
|
+
match py.detach(|| FiscalCalendar::from_json(definition)) {
|
|
41
41
|
Ok(cal) => (Some(Calendar(cal)), None),
|
|
42
42
|
Err(errors) => (None, Some(json::errors_json(errors.as_slice()))),
|
|
43
43
|
}
|