nepkit 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.
nepkit-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kritagya
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.
nepkit-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,350 @@
1
+ Metadata-Version: 2.4
2
+ Name: nepkit
3
+ Version: 0.1.0
4
+ Summary: Typed Bikram Sambat ↔ Gregorian date conversion, as a library and a CLI.
5
+ Keywords: nepal,bikram-sambat,nepali-date,calendar,cli
6
+ Author: Kritagya
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Classifier: Development Status :: 3 - Alpha
10
+ Classifier: Environment :: Console
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
15
+ Classifier: Topic :: Utilities
16
+ Classifier: Typing :: Typed
17
+ Requires-Dist: typer>=0.15,<0.28
18
+ Requires-Python: >=3.12
19
+ Project-URL: Homepage, https://github.com/akakritagya/nepkit
20
+ Project-URL: Issues, https://github.com/akakritagya/nepkit/issues
21
+ Project-URL: Repository, https://github.com/akakritagya/nepkit
22
+ Description-Content-Type: text/markdown
23
+
24
+ # nepkit
25
+
26
+ ```text
27
+ _ _ _
28
+ _ __ ___ _ __ | | __(_)| |_
29
+ | '_ \ / _ \ | '_ \ | |/ /| || __|
30
+ | | | || __/ | |_) || < | || |_
31
+ |_| |_| \___| | .__/ |_|\_\|_| \__|
32
+ |_|
33
+ ```
34
+
35
+ Typed Bikram Sambat ↔ Gregorian date conversion for Python, as a library and a
36
+ command-line tool.
37
+
38
+ > **Status:** first release. The library and CLI both work and are tested; the
39
+ > API may still change before 1.0.
40
+
41
+ [**DEMO.md**](https://github.com/akakritagya/nepkit/blob/main/DEMO.md) walks
42
+ through every command, option, and failure mode with real captured output.
43
+
44
+ ## Why
45
+
46
+ Bikram Sambat is Nepal's official calendar, and converting to and from it is not
47
+ arithmetic. Gregorian leap years follow a rule you can write down; BS month
48
+ lengths do not. They vary between 29 and 32 days with no generating formula, are
49
+ fixed by observation, and are published by Nepal's Panchanga authority. Every
50
+ correct converter is therefore a **lookup table plus one verified anchor date** —
51
+ which means the data matters more than the code, and most of the work in this
52
+ repo went into the data.
53
+
54
+ The existing Python options are small, mostly unmaintained packages with
55
+ undocumented year ranges, no type hints, and no statement of where their numbers
56
+ came from or how far they can be trusted. nepkit aims to be one typed, tested
57
+ converter that is explicit about all three.
58
+
59
+ ## Install
60
+
61
+ Requires Python 3.12+.
62
+
63
+ ```bash
64
+ pip install nepkit # library and CLI
65
+ uv add nepkit # into a uv project
66
+ uv tool install nepkit # the CLI on its own, isolated
67
+ ```
68
+
69
+ [Typer](https://typer.tiangolo.com/) is the only direct dependency. The
70
+ calendar table is bundled, so nepkit never touches the network.
71
+
72
+ ## Library
73
+
74
+ ```python
75
+ from datetime import date
76
+
77
+ from nepkit import BSDate, ad_to_bs, bs_to_ad
78
+
79
+ bs_to_ad(BSDate(2081, 4, 15)) # date(2024, 7, 30)
80
+ ad_to_bs(date(2024, 7, 30)) # BSDate(year=2081, month=4, day=15)
81
+ ```
82
+
83
+ `BSDate` validates on construction, so if you are holding one it is a real date
84
+ in the supported range:
85
+
86
+ ```python
87
+ from nepkit import BSDate, days_in_month
88
+
89
+ days_in_month(2081, 4) # 32
90
+ days_in_month(2081, 9) # 29
91
+
92
+ BSDate(2081, 4, 33) # raises InvalidDateError
93
+ BSDate(2081, 13, 1) # raises InvalidDateError
94
+ BSDate(2095, 1, 1) # raises DateOutOfRangeError
95
+ ```
96
+
97
+ `ad_to_bs` takes a `datetime.date`, so a malformed Gregorian date is impossible
98
+ by construction — Python's own constructor rejects it before nepkit is involved.
99
+
100
+ ### Errors
101
+
102
+ ```text
103
+ NepkitError
104
+ ├── CalendarDataError the bundled table is malformed; raised at import
105
+ └── DateError
106
+ ├── InvalidDateError not a real BS date (month 13, day 33, ...)
107
+ └── DateOutOfRangeError a real date, but outside the bundled range
108
+ ```
109
+
110
+ The split between the last two is the one that earns its keep. BS 2095-03-12 is
111
+ a perfectly real date that nepkit simply has no data for, and a caller can
112
+ reasonably catch that and fall back or report the supported range. BS 2081-13-01
113
+ is not a date at all, and catching it is always a mistake. Catch `DateError` if
114
+ you only need "the user gave me something I can't convert".
115
+
116
+ ## CLI
117
+
118
+ Installing puts a `nepkit` command on your PATH:
119
+
120
+ ```console
121
+ $ nepkit bs2ad 2081-04-15
122
+ 2024-07-30
123
+
124
+ $ nepkit ad2bs 2024-07-30
125
+ 2081-04-15
126
+
127
+ $ nepkit today
128
+ BS 2083-04-26
129
+ AD 2026-08-11
130
+
131
+ $ nepkit range
132
+ BS 2000-01-01 .. 2090-12-30 (years 2000-2090)
133
+ AD 1943-04-14 .. 2034-04-13
134
+ ```
135
+
136
+ Direction is always explicit, and has to be: the BS and AD year numbers overlap
137
+ from 2000 to 2034, so `2024` is a valid year in both calendars and nothing
138
+ could reliably guess which one you meant.
139
+
140
+ ### Interactive
141
+
142
+ Run `nepkit` with no arguments in a terminal and it clears the screen and opens
143
+ a session:
144
+
145
+ ```console
146
+ $ nepkit
147
+ _ _ _
148
+ _ __ ___ _ __ | | __(_)| |_
149
+ | '_ \ / _ \ | '_ \ | |/ /| || __|
150
+ | | | || __/ | |_) || < | || |_
151
+ |_| |_| \___| | .__/ |_|\_\|_| \__|
152
+ |_|
153
+ nepkit v0.1.0 - Bikram Sambat (BS) <-> Gregorian (AD) date conversion
154
+ Today BS 2083-04-26 AD 2026-08-11
155
+
156
+ Type a command, 'help', 'clear', or 'quit'. Up/Down recalls history.
157
+
158
+ nepkit> today
159
+ BS 2083-04-26
160
+ AD 2026-08-11
161
+
162
+ nepkit> bs2ad 2081-04-15
163
+ 2024-07-30
164
+
165
+ nepkit> quit
166
+ ```
167
+
168
+ It accepts exactly the commands above — the same table, not a parallel
169
+ interface — so anything you can type at the shell works here unchanged. A bad
170
+ line reports the error and returns you to the prompt rather than ending the
171
+ session. `clear` (or `cls`) wipes the screen and redraws the banner. `quit`,
172
+ `exit`, `q`, and Ctrl-D all leave; Ctrl-C abandons the current line only.
173
+
174
+ Those four are prompt-only words, not subcommands — `nepkit clear` at a shell
175
+ stays a usage error rather than clearing your terminal.
176
+
177
+ Up and Down recall previous commands, and the usual `readline` editing keys
178
+ work (Ctrl-A, Ctrl-E, Ctrl-R, and so on). History lasts for the session and is
179
+ not written to disk. On Windows, where Python ships no `readline`, the prompt
180
+ works exactly the same minus the editing keys.
181
+
182
+ **Only on a terminal.** With stdin redirected — a pipeline, a script, CI —
183
+ `nepkit` prints help and exits 2 exactly as before, so nothing ever blocks
184
+ waiting for a prompt that isn't there.
185
+
186
+ ### Calendars
187
+
188
+ ```console
189
+ $ nepkit calbs 2081 4
190
+ Shrawan 2081
191
+ 16 Jul - 16 Aug 2024
192
+ Sun Mon Tue Wed Thu Fri Sat
193
+ 1 2 3 4 5
194
+ 6 7 8 9 10 11 12
195
+ 13 14 15 16 17 18 19
196
+ 20 21 22 23 24 25 26
197
+ 27 28 29 30 31 32
198
+
199
+ $ nepkit calad 2024 7
200
+ July 2024
201
+ Ashadh 17 - Shrawan 16, 2081
202
+ Sun Mon Tue Wed Thu Fri Sat
203
+ 1 2 3 4 5 6
204
+ 7 8 9 10 11 12 13
205
+ 14 15 16 17 18 19 20
206
+ 21 22 23 24 25 26 27
207
+ 28 29 30 31
208
+ ```
209
+
210
+ Both default to the current month. A Gregorian month never lines up with a BS
211
+ month, so the subtitle names both ends of the span rather than pretending a
212
+ single corresponding month exists.
213
+
214
+ Grids are boxed and coloured on a terminal and plain when redirected, following
215
+ the same convention as `ls` and `git`. Force it either way with
216
+ `--color always|never|auto`.
217
+
218
+ On a terminal, today's date is picked out in bold bright magenta. The highlight
219
+ exists **only** in the coloured path: piped output is byte-for-byte
220
+ identical whether or not today falls in the month shown, so nothing parsing
221
+ stdout breaks on the one day a month it would otherwise appear. `--json`
222
+ reports it as a `today` field instead, which is `null` when today is elsewhere.
223
+
224
+ ### Scripting
225
+
226
+ Every command takes `--json`:
227
+
228
+ ```console
229
+ $ nepkit ad2bs 2008-05-28 --json
230
+ {"bs": "2065-02-15", "ad": "2008-05-28"}
231
+ ```
232
+
233
+ **stdout carries results, stderr carries errors, and neither ever carries
234
+ both.** stdout contains no ANSI escapes unless you ask for colour explicitly,
235
+ so piping is always safe.
236
+
237
+ Exit codes come straight from the exception hierarchy, so a script can branch
238
+ without parsing any text:
239
+
240
+ | Code | Meaning | Example |
241
+ | --- | --- | --- |
242
+ | 0 | success | |
243
+ | 2 | usage error — bad flag or unknown command | `nepkit nosuchcommand` |
244
+ | 3 | not a real date | `nepkit bs2ad 2081-13-01` |
245
+ | 4 | a real date, but outside the bundled range | `nepkit bs2ad 2095-01-01` |
246
+
247
+ The 3/4 split is the one that matters when scripting: **4 is worth retrying
248
+ against another source, 3 never is.** Collapsing both into `1` would throw that
249
+ distinction away at exactly the boundary where it is most useful.
250
+
251
+ ## Supported range
252
+
253
+ | Calendar | From | To |
254
+ | --- | --- | --- |
255
+ | Bikram Sambat | 2000-01-01 | 2090-12-30 |
256
+ | Gregorian | 1943-04-14 | 2034-04-13 |
257
+
258
+ That is 91 years, 33,238 days. Anything outside it raises `DateOutOfRangeError`
259
+ rather than extrapolating, because there is no rule to extrapolate with — dates
260
+ beyond the table would have to be invented.
261
+
262
+ The bounds are computed from the bundled data, not written down separately, so
263
+ extending the table moves them automatically.
264
+
265
+ ## Design decisions
266
+
267
+ **One anchor, everything else derived.** The whole library hangs on a single
268
+ verified correspondence: BS 2000-01-01 = AD 1943-04-14. That is the only
269
+ Gregorian fact in the package that cannot be computed, because month lengths
270
+ alone cannot tell you where the calendar sits against the Gregorian one. Every
271
+ other bound — the last BS date, both ends of the AD window — is derived from it
272
+ plus the table. A second hardcoded date would be free to drift out of sync, and
273
+ the failure would be silent and total.
274
+
275
+ **Both directions collapse to a day count.** A BS date becomes "days since the
276
+ anchor", integer arithmetic happens there, and the result expands out the other
277
+ side. `datetime.date` is already a correct expander for the Gregorian side, so
278
+ `bs_to_ad` is one line; the real work is the inverse, which has no equivalent in
279
+ the standard library.
280
+
281
+ **Types instead of validation where possible.** `ad_to_bs` accepts a
282
+ `datetime.date` rather than three integers, which removes an entire error class
283
+ from its contract at no cost.
284
+
285
+ ## Limitations
286
+
287
+ - **Dates only.** No time of day, no timezones, no Nepali-language month names
288
+ or numeral formatting.
289
+ - **The range is hard-bounded** at BS 2000–2090 and will not extrapolate.
290
+ - **Correctness rests on the data, and the tests cannot prove it.** The test
291
+ suite verifies self-consistency exhaustively — every one of the 33,238 days
292
+ round-trips, and consecutive day counts produce consecutive dates. But both
293
+ directions read the same table, so a wrong month length cancels out exactly
294
+ and every property still passes. This was verified by deliberately corrupting
295
+ the table: all properties passed while conversions were silently wrong. Only
296
+ the sourcing described in [`src/nepkit/data/DATA.md`](https://github.com/akakritagya/nepkit/blob/main/src/nepkit/data/DATA.md)
297
+ stands behind the numbers themselves.
298
+
299
+ ## Data provenance
300
+
301
+ [`src/nepkit/data/DATA.md`](https://github.com/akakritagya/nepkit/blob/main/src/nepkit/data/DATA.md) records where the calendar
302
+ table came from: two independently maintained sources with different authors,
303
+ languages, and conversion epochs, pinned at specific commits, diffed row by row
304
+ over all 91 years with no disagreements, and cross-checked by walking each
305
+ source forward from its own epoch to the anchor.
306
+
307
+ ## Development
308
+
309
+ This project uses [uv](https://docs.astral.sh/uv/).
310
+
311
+ ```bash
312
+ git clone https://github.com/akakritagya/nepkit
313
+ cd nepkit
314
+ uv sync --group dev # pytest, ruff, mypy, pre-commit
315
+ uv run pytest # run tests
316
+ uv run ruff check . # lint
317
+ uv run ruff format . # format
318
+ uv run mypy # type check
319
+ ```
320
+
321
+ ```bash
322
+ uv run pre-commit install # lint/format/type-check on commit
323
+ uv run pre-commit install --hook-type commit-msg # enforce Conventional Commits
324
+ uv run pre-commit install --hook-type pre-push # full test suite before push
325
+ ```
326
+
327
+ `pre-commit` runs lint, format, and type-check on every commit; blocks large
328
+ files, private keys, and direct commits to `main`; runs the full `pytest` suite
329
+ before `push` (not on every commit — too slow to survive contact with a growing
330
+ suite); and enforces
331
+ [Conventional Commits](https://www.conventionalcommits.org/) on the message.
332
+
333
+ ### Releasing
334
+
335
+ Releases go to PyPI through
336
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — GitHub Actions
337
+ proves the repository's identity over OIDC, so no API token exists to leak or
338
+ rotate. Bump `version` in `pyproject.toml`, then:
339
+
340
+ ```bash
341
+ git tag v0.1.0 && git push origin v0.1.0
342
+ ```
343
+
344
+ The tag runs `.github/workflows/publish.yml`, which re-runs the full gate,
345
+ refuses to continue if the tag disagrees with the packaged version, and only
346
+ then uploads. Run the workflow manually to rehearse against TestPyPI first.
347
+
348
+ ## License
349
+
350
+ MIT — see [LICENSE](https://github.com/akakritagya/nepkit/blob/main/LICENSE).
nepkit-0.1.0/README.md ADDED
@@ -0,0 +1,327 @@
1
+ # nepkit
2
+
3
+ ```text
4
+ _ _ _
5
+ _ __ ___ _ __ | | __(_)| |_
6
+ | '_ \ / _ \ | '_ \ | |/ /| || __|
7
+ | | | || __/ | |_) || < | || |_
8
+ |_| |_| \___| | .__/ |_|\_\|_| \__|
9
+ |_|
10
+ ```
11
+
12
+ Typed Bikram Sambat ↔ Gregorian date conversion for Python, as a library and a
13
+ command-line tool.
14
+
15
+ > **Status:** first release. The library and CLI both work and are tested; the
16
+ > API may still change before 1.0.
17
+
18
+ [**DEMO.md**](https://github.com/akakritagya/nepkit/blob/main/DEMO.md) walks
19
+ through every command, option, and failure mode with real captured output.
20
+
21
+ ## Why
22
+
23
+ Bikram Sambat is Nepal's official calendar, and converting to and from it is not
24
+ arithmetic. Gregorian leap years follow a rule you can write down; BS month
25
+ lengths do not. They vary between 29 and 32 days with no generating formula, are
26
+ fixed by observation, and are published by Nepal's Panchanga authority. Every
27
+ correct converter is therefore a **lookup table plus one verified anchor date** —
28
+ which means the data matters more than the code, and most of the work in this
29
+ repo went into the data.
30
+
31
+ The existing Python options are small, mostly unmaintained packages with
32
+ undocumented year ranges, no type hints, and no statement of where their numbers
33
+ came from or how far they can be trusted. nepkit aims to be one typed, tested
34
+ converter that is explicit about all three.
35
+
36
+ ## Install
37
+
38
+ Requires Python 3.12+.
39
+
40
+ ```bash
41
+ pip install nepkit # library and CLI
42
+ uv add nepkit # into a uv project
43
+ uv tool install nepkit # the CLI on its own, isolated
44
+ ```
45
+
46
+ [Typer](https://typer.tiangolo.com/) is the only direct dependency. The
47
+ calendar table is bundled, so nepkit never touches the network.
48
+
49
+ ## Library
50
+
51
+ ```python
52
+ from datetime import date
53
+
54
+ from nepkit import BSDate, ad_to_bs, bs_to_ad
55
+
56
+ bs_to_ad(BSDate(2081, 4, 15)) # date(2024, 7, 30)
57
+ ad_to_bs(date(2024, 7, 30)) # BSDate(year=2081, month=4, day=15)
58
+ ```
59
+
60
+ `BSDate` validates on construction, so if you are holding one it is a real date
61
+ in the supported range:
62
+
63
+ ```python
64
+ from nepkit import BSDate, days_in_month
65
+
66
+ days_in_month(2081, 4) # 32
67
+ days_in_month(2081, 9) # 29
68
+
69
+ BSDate(2081, 4, 33) # raises InvalidDateError
70
+ BSDate(2081, 13, 1) # raises InvalidDateError
71
+ BSDate(2095, 1, 1) # raises DateOutOfRangeError
72
+ ```
73
+
74
+ `ad_to_bs` takes a `datetime.date`, so a malformed Gregorian date is impossible
75
+ by construction — Python's own constructor rejects it before nepkit is involved.
76
+
77
+ ### Errors
78
+
79
+ ```text
80
+ NepkitError
81
+ ├── CalendarDataError the bundled table is malformed; raised at import
82
+ └── DateError
83
+ ├── InvalidDateError not a real BS date (month 13, day 33, ...)
84
+ └── DateOutOfRangeError a real date, but outside the bundled range
85
+ ```
86
+
87
+ The split between the last two is the one that earns its keep. BS 2095-03-12 is
88
+ a perfectly real date that nepkit simply has no data for, and a caller can
89
+ reasonably catch that and fall back or report the supported range. BS 2081-13-01
90
+ is not a date at all, and catching it is always a mistake. Catch `DateError` if
91
+ you only need "the user gave me something I can't convert".
92
+
93
+ ## CLI
94
+
95
+ Installing puts a `nepkit` command on your PATH:
96
+
97
+ ```console
98
+ $ nepkit bs2ad 2081-04-15
99
+ 2024-07-30
100
+
101
+ $ nepkit ad2bs 2024-07-30
102
+ 2081-04-15
103
+
104
+ $ nepkit today
105
+ BS 2083-04-26
106
+ AD 2026-08-11
107
+
108
+ $ nepkit range
109
+ BS 2000-01-01 .. 2090-12-30 (years 2000-2090)
110
+ AD 1943-04-14 .. 2034-04-13
111
+ ```
112
+
113
+ Direction is always explicit, and has to be: the BS and AD year numbers overlap
114
+ from 2000 to 2034, so `2024` is a valid year in both calendars and nothing
115
+ could reliably guess which one you meant.
116
+
117
+ ### Interactive
118
+
119
+ Run `nepkit` with no arguments in a terminal and it clears the screen and opens
120
+ a session:
121
+
122
+ ```console
123
+ $ nepkit
124
+ _ _ _
125
+ _ __ ___ _ __ | | __(_)| |_
126
+ | '_ \ / _ \ | '_ \ | |/ /| || __|
127
+ | | | || __/ | |_) || < | || |_
128
+ |_| |_| \___| | .__/ |_|\_\|_| \__|
129
+ |_|
130
+ nepkit v0.1.0 - Bikram Sambat (BS) <-> Gregorian (AD) date conversion
131
+ Today BS 2083-04-26 AD 2026-08-11
132
+
133
+ Type a command, 'help', 'clear', or 'quit'. Up/Down recalls history.
134
+
135
+ nepkit> today
136
+ BS 2083-04-26
137
+ AD 2026-08-11
138
+
139
+ nepkit> bs2ad 2081-04-15
140
+ 2024-07-30
141
+
142
+ nepkit> quit
143
+ ```
144
+
145
+ It accepts exactly the commands above — the same table, not a parallel
146
+ interface — so anything you can type at the shell works here unchanged. A bad
147
+ line reports the error and returns you to the prompt rather than ending the
148
+ session. `clear` (or `cls`) wipes the screen and redraws the banner. `quit`,
149
+ `exit`, `q`, and Ctrl-D all leave; Ctrl-C abandons the current line only.
150
+
151
+ Those four are prompt-only words, not subcommands — `nepkit clear` at a shell
152
+ stays a usage error rather than clearing your terminal.
153
+
154
+ Up and Down recall previous commands, and the usual `readline` editing keys
155
+ work (Ctrl-A, Ctrl-E, Ctrl-R, and so on). History lasts for the session and is
156
+ not written to disk. On Windows, where Python ships no `readline`, the prompt
157
+ works exactly the same minus the editing keys.
158
+
159
+ **Only on a terminal.** With stdin redirected — a pipeline, a script, CI —
160
+ `nepkit` prints help and exits 2 exactly as before, so nothing ever blocks
161
+ waiting for a prompt that isn't there.
162
+
163
+ ### Calendars
164
+
165
+ ```console
166
+ $ nepkit calbs 2081 4
167
+ Shrawan 2081
168
+ 16 Jul - 16 Aug 2024
169
+ Sun Mon Tue Wed Thu Fri Sat
170
+ 1 2 3 4 5
171
+ 6 7 8 9 10 11 12
172
+ 13 14 15 16 17 18 19
173
+ 20 21 22 23 24 25 26
174
+ 27 28 29 30 31 32
175
+
176
+ $ nepkit calad 2024 7
177
+ July 2024
178
+ Ashadh 17 - Shrawan 16, 2081
179
+ Sun Mon Tue Wed Thu Fri Sat
180
+ 1 2 3 4 5 6
181
+ 7 8 9 10 11 12 13
182
+ 14 15 16 17 18 19 20
183
+ 21 22 23 24 25 26 27
184
+ 28 29 30 31
185
+ ```
186
+
187
+ Both default to the current month. A Gregorian month never lines up with a BS
188
+ month, so the subtitle names both ends of the span rather than pretending a
189
+ single corresponding month exists.
190
+
191
+ Grids are boxed and coloured on a terminal and plain when redirected, following
192
+ the same convention as `ls` and `git`. Force it either way with
193
+ `--color always|never|auto`.
194
+
195
+ On a terminal, today's date is picked out in bold bright magenta. The highlight
196
+ exists **only** in the coloured path: piped output is byte-for-byte
197
+ identical whether or not today falls in the month shown, so nothing parsing
198
+ stdout breaks on the one day a month it would otherwise appear. `--json`
199
+ reports it as a `today` field instead, which is `null` when today is elsewhere.
200
+
201
+ ### Scripting
202
+
203
+ Every command takes `--json`:
204
+
205
+ ```console
206
+ $ nepkit ad2bs 2008-05-28 --json
207
+ {"bs": "2065-02-15", "ad": "2008-05-28"}
208
+ ```
209
+
210
+ **stdout carries results, stderr carries errors, and neither ever carries
211
+ both.** stdout contains no ANSI escapes unless you ask for colour explicitly,
212
+ so piping is always safe.
213
+
214
+ Exit codes come straight from the exception hierarchy, so a script can branch
215
+ without parsing any text:
216
+
217
+ | Code | Meaning | Example |
218
+ | --- | --- | --- |
219
+ | 0 | success | |
220
+ | 2 | usage error — bad flag or unknown command | `nepkit nosuchcommand` |
221
+ | 3 | not a real date | `nepkit bs2ad 2081-13-01` |
222
+ | 4 | a real date, but outside the bundled range | `nepkit bs2ad 2095-01-01` |
223
+
224
+ The 3/4 split is the one that matters when scripting: **4 is worth retrying
225
+ against another source, 3 never is.** Collapsing both into `1` would throw that
226
+ distinction away at exactly the boundary where it is most useful.
227
+
228
+ ## Supported range
229
+
230
+ | Calendar | From | To |
231
+ | --- | --- | --- |
232
+ | Bikram Sambat | 2000-01-01 | 2090-12-30 |
233
+ | Gregorian | 1943-04-14 | 2034-04-13 |
234
+
235
+ That is 91 years, 33,238 days. Anything outside it raises `DateOutOfRangeError`
236
+ rather than extrapolating, because there is no rule to extrapolate with — dates
237
+ beyond the table would have to be invented.
238
+
239
+ The bounds are computed from the bundled data, not written down separately, so
240
+ extending the table moves them automatically.
241
+
242
+ ## Design decisions
243
+
244
+ **One anchor, everything else derived.** The whole library hangs on a single
245
+ verified correspondence: BS 2000-01-01 = AD 1943-04-14. That is the only
246
+ Gregorian fact in the package that cannot be computed, because month lengths
247
+ alone cannot tell you where the calendar sits against the Gregorian one. Every
248
+ other bound — the last BS date, both ends of the AD window — is derived from it
249
+ plus the table. A second hardcoded date would be free to drift out of sync, and
250
+ the failure would be silent and total.
251
+
252
+ **Both directions collapse to a day count.** A BS date becomes "days since the
253
+ anchor", integer arithmetic happens there, and the result expands out the other
254
+ side. `datetime.date` is already a correct expander for the Gregorian side, so
255
+ `bs_to_ad` is one line; the real work is the inverse, which has no equivalent in
256
+ the standard library.
257
+
258
+ **Types instead of validation where possible.** `ad_to_bs` accepts a
259
+ `datetime.date` rather than three integers, which removes an entire error class
260
+ from its contract at no cost.
261
+
262
+ ## Limitations
263
+
264
+ - **Dates only.** No time of day, no timezones, no Nepali-language month names
265
+ or numeral formatting.
266
+ - **The range is hard-bounded** at BS 2000–2090 and will not extrapolate.
267
+ - **Correctness rests on the data, and the tests cannot prove it.** The test
268
+ suite verifies self-consistency exhaustively — every one of the 33,238 days
269
+ round-trips, and consecutive day counts produce consecutive dates. But both
270
+ directions read the same table, so a wrong month length cancels out exactly
271
+ and every property still passes. This was verified by deliberately corrupting
272
+ the table: all properties passed while conversions were silently wrong. Only
273
+ the sourcing described in [`src/nepkit/data/DATA.md`](https://github.com/akakritagya/nepkit/blob/main/src/nepkit/data/DATA.md)
274
+ stands behind the numbers themselves.
275
+
276
+ ## Data provenance
277
+
278
+ [`src/nepkit/data/DATA.md`](https://github.com/akakritagya/nepkit/blob/main/src/nepkit/data/DATA.md) records where the calendar
279
+ table came from: two independently maintained sources with different authors,
280
+ languages, and conversion epochs, pinned at specific commits, diffed row by row
281
+ over all 91 years with no disagreements, and cross-checked by walking each
282
+ source forward from its own epoch to the anchor.
283
+
284
+ ## Development
285
+
286
+ This project uses [uv](https://docs.astral.sh/uv/).
287
+
288
+ ```bash
289
+ git clone https://github.com/akakritagya/nepkit
290
+ cd nepkit
291
+ uv sync --group dev # pytest, ruff, mypy, pre-commit
292
+ uv run pytest # run tests
293
+ uv run ruff check . # lint
294
+ uv run ruff format . # format
295
+ uv run mypy # type check
296
+ ```
297
+
298
+ ```bash
299
+ uv run pre-commit install # lint/format/type-check on commit
300
+ uv run pre-commit install --hook-type commit-msg # enforce Conventional Commits
301
+ uv run pre-commit install --hook-type pre-push # full test suite before push
302
+ ```
303
+
304
+ `pre-commit` runs lint, format, and type-check on every commit; blocks large
305
+ files, private keys, and direct commits to `main`; runs the full `pytest` suite
306
+ before `push` (not on every commit — too slow to survive contact with a growing
307
+ suite); and enforces
308
+ [Conventional Commits](https://www.conventionalcommits.org/) on the message.
309
+
310
+ ### Releasing
311
+
312
+ Releases go to PyPI through
313
+ [Trusted Publishing](https://docs.pypi.org/trusted-publishers/) — GitHub Actions
314
+ proves the repository's identity over OIDC, so no API token exists to leak or
315
+ rotate. Bump `version` in `pyproject.toml`, then:
316
+
317
+ ```bash
318
+ git tag v0.1.0 && git push origin v0.1.0
319
+ ```
320
+
321
+ The tag runs `.github/workflows/publish.yml`, which re-runs the full gate,
322
+ refuses to continue if the tag disagrees with the packaged version, and only
323
+ then uploads. Run the workflow manually to rehearse against TestPyPI first.
324
+
325
+ ## License
326
+
327
+ MIT — see [LICENSE](https://github.com/akakritagya/nepkit/blob/main/LICENSE).