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.
Files changed (38) hide show
  1. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/Cargo.lock +3 -3
  2. fiscalclock-0.2.0/PKG-INFO +180 -0
  3. fiscalclock-0.2.0/README.md +147 -0
  4. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/Cargo.toml +2 -2
  5. fiscalclock-0.2.0/bindings/python/README.md +147 -0
  6. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/src/lib.rs +6 -6
  7. fiscalclock-0.2.0/bindings/python/tests/test_api.py +335 -0
  8. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/Cargo.toml +2 -1
  9. fiscalclock-0.2.0/crates/core/README.md +148 -0
  10. fiscalclock-0.2.0/crates/core/src/calendar.rs +979 -0
  11. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/definition.rs +728 -307
  12. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/dimension.rs +88 -66
  13. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/error.rs +25 -9
  14. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/explain.rs +101 -60
  15. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/json.rs +56 -49
  16. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/label.rs +65 -51
  17. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/lib.rs +6 -2
  18. fiscalclock-0.2.0/crates/core/src/timeline.rs +678 -0
  19. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/pyproject.toml +2 -2
  20. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/python/fiscalclock/__init__.py +56 -10
  21. fiscalclock-0.1.0/PKG-INFO +0 -119
  22. fiscalclock-0.1.0/README.md +0 -86
  23. fiscalclock-0.1.0/bindings/python/README.md +0 -86
  24. fiscalclock-0.1.0/bindings/python/tests/test_api.py +0 -145
  25. fiscalclock-0.1.0/crates/core/README.md +0 -80
  26. fiscalclock-0.1.0/crates/core/src/calendar.rs +0 -1055
  27. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/Cargo.toml +0 -0
  28. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/LICENSE-APACHE +0 -0
  29. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/LICENSE-MIT +0 -0
  30. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/LICENSE-APACHE +0 -0
  31. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/LICENSE-MIT +0 -0
  32. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/bindings/python/tests/test_conformance.py +0 -0
  33. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/LICENSE-APACHE +0 -0
  34. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/LICENSE-MIT +0 -0
  35. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/date.rs +0 -0
  36. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/crates/core/src/strict.rs +0 -0
  37. {fiscalclock-0.1.0 → fiscalclock-0.2.0}/python/fiscalclock/_fiscalclock.pyi +0 -0
  38. {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.1.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.1.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.1.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
+ [![PyPI](https://img.shields.io/pypi/v/fiscalclock?label=PyPI)](https://pypi.org/project/fiscalclock/)
36
+ [![Python versions](https://img.shields.io/pypi/pyversions/fiscalclock)](https://pypi.org/project/fiscalclock/)
37
+ [![CI](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml)
38
+ [![Licence: MIT OR Apache-2.0](https://img.shields.io/badge/licence-MIT%20OR%20Apache--2.0-blue)](#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
+ [![PyPI](https://img.shields.io/pypi/v/fiscalclock?label=PyPI)](https://pypi.org/project/fiscalclock/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/fiscalclock)](https://pypi.org/project/fiscalclock/)
5
+ [![CI](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml)
6
+ [![Licence: MIT OR Apache-2.0](https://img.shields.io/badge/licence-MIT%20OR%20Apache--2.0-blue)](#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.1.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.1.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
+ [![PyPI](https://img.shields.io/pypi/v/fiscalclock?label=PyPI)](https://pypi.org/project/fiscalclock/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/fiscalclock)](https://pypi.org/project/fiscalclock/)
5
+ [![CI](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/Karthik-beta/fiscalclock/actions/workflows/ci.yml)
6
+ [![Licence: MIT OR Apache-2.0](https://img.shields.io/badge/licence-MIT%20OR%20Apache--2.0-blue)](#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
  }