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 +21 -0
- nepkit-0.1.0/PKG-INFO +350 -0
- nepkit-0.1.0/README.md +327 -0
- nepkit-0.1.0/pyproject.toml +95 -0
- nepkit-0.1.0/pyproject.toml.orig +74 -0
- nepkit-0.1.0/src/nepkit/__init__.py +32 -0
- nepkit-0.1.0/src/nepkit/calendar_data.py +202 -0
- nepkit-0.1.0/src/nepkit/cli.py +384 -0
- nepkit-0.1.0/src/nepkit/convert.py +52 -0
- nepkit-0.1.0/src/nepkit/data/DATA.md +52 -0
- nepkit-0.1.0/src/nepkit/data/__init__.py +0 -0
- nepkit-0.1.0/src/nepkit/data/calendar.json +93 -0
- nepkit-0.1.0/src/nepkit/exceptions.py +18 -0
- nepkit-0.1.0/src/nepkit/py.typed +0 -0
- nepkit-0.1.0/src/nepkit/render.py +159 -0
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).
|