praytime 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.
praytime-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2007-2025 Hamid Zarrabi-Zadeh
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.
@@ -0,0 +1,348 @@
1
+ Metadata-Version: 2.4
2
+ Name: praytime
3
+ Version: 0.1.0
4
+ Summary: Zero-dependency Islamic prayer times calculator (Python port of zarrabi/praytime)
5
+ Author: Milad Khajavi
6
+ Maintainer: Milad Khajavi
7
+ License-Expression: MIT
8
+ Project-URL: Homepage, https://github.com/khajavi/praytime
9
+ Project-URL: Repository, https://github.com/khajavi/praytime
10
+ Project-URL: Documentation, https://github.com/khajavi/praytime#readme
11
+ Project-URL: Issues, https://github.com/khajavi/praytime/issues
12
+ Project-URL: Upstream (zarrabi/praytime), https://github.com/zarrabi/praytime
13
+ Keywords: prayer,times,islam,astronomy,salah,praytime
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3.9
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Topic :: Religion
24
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
+ Requires-Python: >=3.9
26
+ Description-Content-Type: text/markdown
27
+ License-File: LICENSE
28
+ Provides-Extra: dev
29
+ Requires-Dist: pytest; extra == "dev"
30
+ Requires-Dist: ruff; extra == "dev"
31
+ Requires-Dist: mypy<2,>=1.0; extra == "dev"
32
+ Dynamic: license-file
33
+
34
+ # praytime
35
+
36
+ > [!WARNING]
37
+ > **Disclaimer.** This library is a port of the original
38
+ > [`zarrabi/praytime`](https://github.com/zarrabi/praytime) produced using
39
+ > AI-agent code-generation tooling. Despite extensive testing against the
40
+ > upstream implementation, it may contain bugs in prayer-time calculations.
41
+ > Please use it at your own responsibility and verify the times before relying
42
+ > on them. See [Disclaimer](#disclaimer) for details.
43
+
44
+ Zero-dependency Islamic prayer times calculator — a faithful Python port of
45
+ [`zarrabi/praytime`](https://github.com/zarrabi/praytime) (v3.2.0).
46
+
47
+ It computes the eight daily prayer times — **Fajr, Sunrise, Dhuhr, Asr, Sunset,
48
+ Maghrib, Isha, Midnight** — from a date and `[latitude, longitude]` using the
49
+ same standard astronomical formulas as the JavaScript library (USNO approximate
50
+ solar coordinates + hour-angle trigonometry). The port reproduces the JS
51
+ numerical behaviour exactly, including its deliberate quirks, while exposing an
52
+ idiomatic, chainable, fully type-hinted Python API.
53
+
54
+ ## Status
55
+
56
+ Feature-complete and release-ready: the full API, the 11 calculation methods,
57
+ the four high-latitude rules, all output formats, date-input handling, and the
58
+ high-latitude/interval overrides are implemented and covered by a comprehensive
59
+ pytest suite that includes snapshots generated directly from the upstream
60
+ JavaScript library.
61
+
62
+ - Python **3.9+**
63
+ - **Zero runtime dependencies** (standard library only: `math`, `datetime`,
64
+ `zoneinfo`, `re`, `calendar`)
65
+ - Ships `py.typed` for downstream type checkers
66
+
67
+ ## Installation
68
+
69
+ ```bash
70
+ pip install praytime
71
+ ```
72
+
73
+ For development (installs pytest, ruff, and mypy):
74
+
75
+ ```bash
76
+ pip install -e ".[dev]"
77
+ python -m pytest -q
78
+ ```
79
+
80
+ ## Quick start
81
+
82
+ ```python
83
+ from praytime import PrayTime
84
+
85
+ praytime = PrayTime("ISNA").location([43.4643, -80.5204]).timezone("America/Toronto")
86
+ times = praytime.times([2025, 2, 21]) # 1-based month: 21 Feb 2025
87
+
88
+ times["fajr"] # '05:52'
89
+ times["dhuhr"] # '12:36'
90
+ times["asr"] # '15:32'
91
+ times["maghrib"] # '18:02'
92
+ ```
93
+
94
+ All setters return `self`, so configuration chains:
95
+
96
+ ```python
97
+ from praytime import PrayTime
98
+
99
+ times = (
100
+ PrayTime("MWL")
101
+ .location([51.5074, -0.1278])
102
+ .timezone("Europe/London")
103
+ .adjust({"asr": "Hanafi", "highLats": "AngleBased"})
104
+ .tune({"fajr": -2, "isha": 3}) # minutes
105
+ .round("nearest")
106
+ .format("12h")
107
+ .times([2025, 6, 21])
108
+ )
109
+ # {'fajr': '2:29 AM', 'sunrise': '4:43 AM', 'dhuhr': '1:02 PM', 'asr': '6:40 PM', ...}
110
+ ```
111
+
112
+ `times()` accepts:
113
+ - nothing / `0` → today;
114
+ - an `int`/`float` `< 1000` → a day offset from today (may be fractional);
115
+ - an `int`/`float` `>= 1000` → a Unix **milliseconds** timestamp;
116
+ - a `datetime.date` / `datetime.datetime` (the time-of-day is discarded);
117
+ - a 1-based `(year, month, day)` tuple or list.
118
+
119
+ ```python
120
+ praytime.times() # today
121
+ praytime.times(7) # one week from today
122
+ praytime.times([2025, 2, 21]) # 21 Feb 2025
123
+ ```
124
+
125
+ > The CLI additionally accepts an ISO `YYYY-MM-DD` string for `--date`.
126
+
127
+ ## Returned keys
128
+
129
+ `times()` (and `get_times()`) return a `dict` with exactly these eight keys, in
130
+ this order:
131
+
132
+ | Key | Prayer |
133
+ |---|---|
134
+ | `fajr` | Fajr |
135
+ | `sunrise` | Sunrise |
136
+ | `dhuhr` | Dhuhr |
137
+ | `asr` | Asr |
138
+ | `sunset` | Sunset |
139
+ | `maghrib` | Maghrib |
140
+ | `isha` | Isha |
141
+ | `midnight` | Midnight |
142
+
143
+ Values are formatted **strings** (`"05:52"`, `"3:32 PM"`, `"-----"`) for string
144
+ formats, and **ints** for the `"x"` (Unix ms) / `"X"` (Unix s) formats. A `nan`
145
+ result (e.g. Fajr at a polar latitude with `highLats="None"`) renders as
146
+ `"-----"`.
147
+
148
+ ## Calculation methods
149
+
150
+ Select a method by name via the constructor or `.method("...")`. Every method is
151
+ applied on top of the shared `defaults` (`isha: 14°`, `maghrib: '1 min'`,
152
+ `midnight: 'Standard'`), so a method only overrides the keys it defines — e.g.
153
+ Tehran and Jafari inherit `isha: 14°`.
154
+
155
+ | Method | `fajr` | `isha` | `maghrib` | `midnight` | Convention |
156
+ |---|---|---|---|---|---|
157
+ | `MWL` (default) | 18° | 17° | `'1 min'` | `'Standard'` | Muslim World League |
158
+ | `ISNA` | 15° | 15° | `'1 min'` | `'Standard'` | Islamic Society of North America |
159
+ | `Egypt` | 19.5° | 17.5° | `'1 min'` | `'Standard'` | Egyptian General Authority of Survey |
160
+ | `Makkah` | 18.5° | `'90 min'` | `'1 min'` | `'Standard'` | Umm al-Qura |
161
+ | `Karachi` | 18° | 18° | `'1 min'` | `'Standard'` | Univ. of Islamic Sciences, Karachi |
162
+ | `Tehran` | 17.7° | 14° (inherited) | 4.5° | `'Jafari'` | Inst. of Geophysics, Univ. of Tehran |
163
+ | `Jafari` | 16° | 14° (inherited) | 4° | `'Jafari'` | Leva Research Institute, Qom |
164
+ | `France` | 12° | 12° | `'1 min'` | `'Standard'` | Union des Organisations Islamiques de France |
165
+ | `Russia` | 16° | 15° | `'1 min'` | `'Standard'` | Spiritual Administration of Muslims of Russia |
166
+ | `Malaysia` | 20° | 18° | `'1 min'` | `'Standard'` | JAKIM |
167
+ | `Singapore` | 20° | 18° | `'1 min'` | `'Standard'` | Islamic Religious Council of Singapore |
168
+
169
+ An angle is the sun's depression below the horizon; a `'<n> min'` value is a
170
+ fixed interval (Maghrib after Sunset, Isha after Maghrib). You can add your own
171
+ method to the per-instance table:
172
+
173
+ ```python
174
+ praytime.methods["Algeria"] = {"fajr": 18, "isha": 17}
175
+ praytime.method("Algeria")
176
+ ```
177
+
178
+ ## Madhab / Asr
179
+
180
+ The Asr shadow length is set through the `asr` parameter (default `"Standard"`):
181
+
182
+ | `asr` | Meaning | Shadow factor |
183
+ |---|---|---|
184
+ | `"Standard"` | Shafi'i, Maliki, Jafari, Hanbali | 1 |
185
+ | `"Hanafi"` | Hanafi school | 2 |
186
+ | a number or numeric string (e.g. `1.7`) | custom shadow factor | parsed value |
187
+
188
+ ```python
189
+ PrayTime().adjust({"asr": "Hanafi"})
190
+ ```
191
+
192
+ ## Options
193
+
194
+ | Setter | Values | Default | Notes |
195
+ |---|---|---|---|
196
+ | `.location([lat, lng])` | degrees | host-derived | latitude, longitude |
197
+ | `.timezone("...")` | IANA name | host zone | used when `utcOffset` is `"auto"` |
198
+ | `.adjust({...})` | any settings mapping | — | generic merge for `fajr`, `isha`, `dhuhr`, `maghrib`, `midnight`, `asr`, `highLats`, … |
199
+ | `.tune({...})` | prayer → minutes | `{}` | per-prayer offsets; fractions allowed |
200
+ | `.round(value)` | `"nearest"`, `"up"`, `"down"`, `"none"` | `"nearest"` | minute rounding (`nearest` is JS half-up) |
201
+ | `.format(value)` | `"24h"`, `"12h"`, `"12H"`, `"x"`, `"X"`, or a callable | `"24h"` | see formats below |
202
+ | `.utc_offset(value)` | number (minutes) or `"auto"` | `"auto"` | see the heuristic below |
203
+ | `.set({...})` | any settings mapping | — | low-level merge used by every setter |
204
+
205
+ ### Output formats
206
+
207
+ | `format` | Result (Asr example) |
208
+ |---|---|
209
+ | `"24h"` | `'15:32'` (zero-padded hour) |
210
+ | `"12h"` | `'3:32 PM'` (no leading zero) |
211
+ | `"12H"` | `'3:32'` (AM/PM stripped) |
212
+ | `"x"` | `1740169920000` (Unix milliseconds, `int`) |
213
+ | `"X"` | `1740169920` (Unix seconds, `int`) |
214
+ | callable | the callable's return value; it receives the rounded ms `int` |
215
+
216
+ ### `utc_offset` heuristic
217
+
218
+ `utc_offset(n)` treats a numeric `n` with `abs(n) < 16` as **hours** (`n * 60`),
219
+ and larger values as **minutes**. It also forces `timezone` to `"UTC"` and sets
220
+ `utcOffset` to the numeric value:
221
+
222
+ ```python
223
+ PrayTime().utc_offset(-5) # -300 minutes (UTC-5)
224
+ PrayTime().utc_offset(-4.5) # -270 minutes
225
+ PrayTime().utc_offset(-300) # -300 minutes (already minutes)
226
+ ```
227
+
228
+ A numeric `utcOffset` shifts the timestamp before rendering; `"auto"` renders in
229
+ the configured `timezone` instead. (For parity, calling `utc_offset("auto")`
230
+ forces `timezone="UTC"`.)
231
+
232
+ ### High-latitude rules
233
+
234
+ Controlled by `highLats` (default `"NightMiddle"`). They clamp Fajr/Isha/Maghrib
235
+ when the sun does not reach the required depression angle.
236
+
237
+ | `highLats` | Factor | Clamp |
238
+ |---|---|---|
239
+ | `"None"` | — | no adjustment (polar results may render as `'-----'`) |
240
+ | `"NightMiddle"` | 1/2 | Fajr = sunrise − night/2; Isha/Maghrib = sunset + night/2 |
241
+ | `"OneSeventh"` | 1/7 | Fajr = sunrise − night/7; Isha/Maghrib = sunset + night/7 |
242
+ | `"AngleBased"` | (1/60) × angle | proportional to the twilight angle |
243
+
244
+ where `night = 24 + sunrise − sunset`.
245
+
246
+ ## Backward-compatible `get_times`
247
+
248
+ ```python
249
+ praytime.get_times(date, location, timezone="auto", dst=0, format="24h")
250
+ ```
251
+
252
+ When `location` is omitted/falsy it delegates to `times(date)`. Otherwise it sets
253
+ the location, a numeric UTC offset (`timezone + dst`), and the format, then
254
+ computes. Note the third parameter is a **numeric UTC offset** (or `"auto"`),
255
+ matching the JS library — not an IANA zone name.
256
+
257
+ ## Command line
258
+
259
+ The package is runnable as `python -m praytime`, and installing it provides a
260
+ `praytime` console command:
261
+
262
+ ```bash
263
+ python -m praytime \
264
+ --method ISNA \
265
+ --lat 43.4643 --lng -80.5204 \
266
+ --timezone America/Toronto \
267
+ --date 2025-02-21
268
+ ```
269
+
270
+ ```text
271
+ Fajr 05:52
272
+ Sunrise 07:11
273
+ Dhuhr 12:36
274
+ Asr 15:32
275
+ Sunset 18:01
276
+ Maghrib 18:02
277
+ Isha 19:20
278
+ Midnight 00:36
279
+ ```
280
+
281
+ Options:
282
+
283
+ | Option | Default | Choices |
284
+ |---|---|---|
285
+ | `--method` | `MWL` | the 11 method names above |
286
+ | `--lat` | *(required)* | latitude in degrees |
287
+ | `--lng` | *(required)* | longitude in degrees |
288
+ | `--timezone` | `UTC` | IANA timezone name |
289
+ | `--date` | today | `YYYY-MM-DD` |
290
+ | `--format` | `24h` | `24h`, `12h`, `12H`, `x`, `X` |
291
+ | `--asr` | `Standard` | `Standard`, `Hanafi` |
292
+ | `--high-lats` | `NightMiddle` | `None`, `NightMiddle`, `OneSeventh`, `AngleBased` |
293
+
294
+ ## Compatibility & intentional deviations from the JS library
295
+
296
+ The port is numerically faithful (see the spec's §13.1 parity list). The
297
+ following differ intentionally, and only in ergonomics or error handling
298
+ (spec §13.2):
299
+
300
+ - **snake_case public methods.** `getTimes` → `get_times`, `utcOffset` →
301
+ `utc_offset`, `setMethod` → `set_method` (deprecated, returns `None`). The
302
+ class name stays `PrayTime`.
303
+ - **camelCase settings keys are preserved verbatim.** Keys consumed by the
304
+ algorithm and passed through `adjust({...})` — `highLats`, `utcOffset` — keep
305
+ their JS spelling so drop-in translations keep working.
306
+ - **Stricter invalid-date handling.** Malformed `(y, m, d)` tuples (wrong length)
307
+ and out-of-range dates raise `ValueError`; the JS `Date.UTC` silently
308
+ normalizes them. A wrong-length sequence raises
309
+ `ValueError("date must be (year, month, day)")`.
310
+ - **Type hints and private internals.** Full annotations are provided; the JS
311
+ instance fields `utcTime` / `adjusted` become the private `_utc_time` /
312
+ `_adjusted`. Observable behaviour is unchanged.
313
+ - **Per-instance method table.** `praytime.methods` is a deep copy, so mutating
314
+ one instance (e.g. adding `"Algeria"`) never leaks to another. The user
315
+ workflow `praytime.methods["X"] = {...}` is unchanged.
316
+ - **Unknown method names** apply only the shared `defaults` and do **not** raise,
317
+ matching the JS no-op.
318
+ - **Timezone rendering** uses `zoneinfo` (stdlib) rather than the host `Intl`
319
+ database; `12h` hours are rendered arithmetically (no leading zero) for
320
+ deterministic, portable output.
321
+ - **Preserved JS quirks** (not deviations, but worth knowing): the
322
+ `abs(n) < 16` hours heuristic in `utc_offset`, the `date < 1000` day-offset
323
+ heuristic in `times`, the stateful high-latitude `_adjusted` flag, and
324
+ `get_times`' numeric-offset third parameter.
325
+
326
+ The implementation specification lives in [`docs/specs.md`](docs/specs.md) and
327
+ the roadmap in [`docs/plan.md`](docs/plan.md).
328
+
329
+ ## Disclaimer
330
+
331
+ > [!WARNING]
332
+ > This library is a Python port of the original
333
+ > [`zarrabi/praytime`](https://github.com/zarrabi/praytime) library, produced
334
+ > with AI-agent code-generation tooling ("agent-driven development"). Although
335
+ > it is tested extensively against the upstream JavaScript implementation —
336
+ > including a pytest suite and thousands of golden snapshots generated directly
337
+ > from the original code — automated translation can still introduce bugs, so
338
+ > the prayer-time calculations may be incorrect in some cases.
339
+ >
340
+ > Always verify the computed times against an authoritative local source before
341
+ > relying on them for worship. This software is provided **"as is", without
342
+ > warranty of any kind**, and the authors and contributors accept no liability
343
+ > for its use — **you use it at your own responsibility.**
344
+
345
+ ## License
346
+
347
+ MIT — see [`LICENSE`](LICENSE). Preserves the upstream copyright notice:
348
+ `Copyright (c) 2007-2025 Hamid Zarrabi-Zadeh`.
@@ -0,0 +1,315 @@
1
+ # praytime
2
+
3
+ > [!WARNING]
4
+ > **Disclaimer.** This library is a port of the original
5
+ > [`zarrabi/praytime`](https://github.com/zarrabi/praytime) produced using
6
+ > AI-agent code-generation tooling. Despite extensive testing against the
7
+ > upstream implementation, it may contain bugs in prayer-time calculations.
8
+ > Please use it at your own responsibility and verify the times before relying
9
+ > on them. See [Disclaimer](#disclaimer) for details.
10
+
11
+ Zero-dependency Islamic prayer times calculator — a faithful Python port of
12
+ [`zarrabi/praytime`](https://github.com/zarrabi/praytime) (v3.2.0).
13
+
14
+ It computes the eight daily prayer times — **Fajr, Sunrise, Dhuhr, Asr, Sunset,
15
+ Maghrib, Isha, Midnight** — from a date and `[latitude, longitude]` using the
16
+ same standard astronomical formulas as the JavaScript library (USNO approximate
17
+ solar coordinates + hour-angle trigonometry). The port reproduces the JS
18
+ numerical behaviour exactly, including its deliberate quirks, while exposing an
19
+ idiomatic, chainable, fully type-hinted Python API.
20
+
21
+ ## Status
22
+
23
+ Feature-complete and release-ready: the full API, the 11 calculation methods,
24
+ the four high-latitude rules, all output formats, date-input handling, and the
25
+ high-latitude/interval overrides are implemented and covered by a comprehensive
26
+ pytest suite that includes snapshots generated directly from the upstream
27
+ JavaScript library.
28
+
29
+ - Python **3.9+**
30
+ - **Zero runtime dependencies** (standard library only: `math`, `datetime`,
31
+ `zoneinfo`, `re`, `calendar`)
32
+ - Ships `py.typed` for downstream type checkers
33
+
34
+ ## Installation
35
+
36
+ ```bash
37
+ pip install praytime
38
+ ```
39
+
40
+ For development (installs pytest, ruff, and mypy):
41
+
42
+ ```bash
43
+ pip install -e ".[dev]"
44
+ python -m pytest -q
45
+ ```
46
+
47
+ ## Quick start
48
+
49
+ ```python
50
+ from praytime import PrayTime
51
+
52
+ praytime = PrayTime("ISNA").location([43.4643, -80.5204]).timezone("America/Toronto")
53
+ times = praytime.times([2025, 2, 21]) # 1-based month: 21 Feb 2025
54
+
55
+ times["fajr"] # '05:52'
56
+ times["dhuhr"] # '12:36'
57
+ times["asr"] # '15:32'
58
+ times["maghrib"] # '18:02'
59
+ ```
60
+
61
+ All setters return `self`, so configuration chains:
62
+
63
+ ```python
64
+ from praytime import PrayTime
65
+
66
+ times = (
67
+ PrayTime("MWL")
68
+ .location([51.5074, -0.1278])
69
+ .timezone("Europe/London")
70
+ .adjust({"asr": "Hanafi", "highLats": "AngleBased"})
71
+ .tune({"fajr": -2, "isha": 3}) # minutes
72
+ .round("nearest")
73
+ .format("12h")
74
+ .times([2025, 6, 21])
75
+ )
76
+ # {'fajr': '2:29 AM', 'sunrise': '4:43 AM', 'dhuhr': '1:02 PM', 'asr': '6:40 PM', ...}
77
+ ```
78
+
79
+ `times()` accepts:
80
+ - nothing / `0` → today;
81
+ - an `int`/`float` `< 1000` → a day offset from today (may be fractional);
82
+ - an `int`/`float` `>= 1000` → a Unix **milliseconds** timestamp;
83
+ - a `datetime.date` / `datetime.datetime` (the time-of-day is discarded);
84
+ - a 1-based `(year, month, day)` tuple or list.
85
+
86
+ ```python
87
+ praytime.times() # today
88
+ praytime.times(7) # one week from today
89
+ praytime.times([2025, 2, 21]) # 21 Feb 2025
90
+ ```
91
+
92
+ > The CLI additionally accepts an ISO `YYYY-MM-DD` string for `--date`.
93
+
94
+ ## Returned keys
95
+
96
+ `times()` (and `get_times()`) return a `dict` with exactly these eight keys, in
97
+ this order:
98
+
99
+ | Key | Prayer |
100
+ |---|---|
101
+ | `fajr` | Fajr |
102
+ | `sunrise` | Sunrise |
103
+ | `dhuhr` | Dhuhr |
104
+ | `asr` | Asr |
105
+ | `sunset` | Sunset |
106
+ | `maghrib` | Maghrib |
107
+ | `isha` | Isha |
108
+ | `midnight` | Midnight |
109
+
110
+ Values are formatted **strings** (`"05:52"`, `"3:32 PM"`, `"-----"`) for string
111
+ formats, and **ints** for the `"x"` (Unix ms) / `"X"` (Unix s) formats. A `nan`
112
+ result (e.g. Fajr at a polar latitude with `highLats="None"`) renders as
113
+ `"-----"`.
114
+
115
+ ## Calculation methods
116
+
117
+ Select a method by name via the constructor or `.method("...")`. Every method is
118
+ applied on top of the shared `defaults` (`isha: 14°`, `maghrib: '1 min'`,
119
+ `midnight: 'Standard'`), so a method only overrides the keys it defines — e.g.
120
+ Tehran and Jafari inherit `isha: 14°`.
121
+
122
+ | Method | `fajr` | `isha` | `maghrib` | `midnight` | Convention |
123
+ |---|---|---|---|---|---|
124
+ | `MWL` (default) | 18° | 17° | `'1 min'` | `'Standard'` | Muslim World League |
125
+ | `ISNA` | 15° | 15° | `'1 min'` | `'Standard'` | Islamic Society of North America |
126
+ | `Egypt` | 19.5° | 17.5° | `'1 min'` | `'Standard'` | Egyptian General Authority of Survey |
127
+ | `Makkah` | 18.5° | `'90 min'` | `'1 min'` | `'Standard'` | Umm al-Qura |
128
+ | `Karachi` | 18° | 18° | `'1 min'` | `'Standard'` | Univ. of Islamic Sciences, Karachi |
129
+ | `Tehran` | 17.7° | 14° (inherited) | 4.5° | `'Jafari'` | Inst. of Geophysics, Univ. of Tehran |
130
+ | `Jafari` | 16° | 14° (inherited) | 4° | `'Jafari'` | Leva Research Institute, Qom |
131
+ | `France` | 12° | 12° | `'1 min'` | `'Standard'` | Union des Organisations Islamiques de France |
132
+ | `Russia` | 16° | 15° | `'1 min'` | `'Standard'` | Spiritual Administration of Muslims of Russia |
133
+ | `Malaysia` | 20° | 18° | `'1 min'` | `'Standard'` | JAKIM |
134
+ | `Singapore` | 20° | 18° | `'1 min'` | `'Standard'` | Islamic Religious Council of Singapore |
135
+
136
+ An angle is the sun's depression below the horizon; a `'<n> min'` value is a
137
+ fixed interval (Maghrib after Sunset, Isha after Maghrib). You can add your own
138
+ method to the per-instance table:
139
+
140
+ ```python
141
+ praytime.methods["Algeria"] = {"fajr": 18, "isha": 17}
142
+ praytime.method("Algeria")
143
+ ```
144
+
145
+ ## Madhab / Asr
146
+
147
+ The Asr shadow length is set through the `asr` parameter (default `"Standard"`):
148
+
149
+ | `asr` | Meaning | Shadow factor |
150
+ |---|---|---|
151
+ | `"Standard"` | Shafi'i, Maliki, Jafari, Hanbali | 1 |
152
+ | `"Hanafi"` | Hanafi school | 2 |
153
+ | a number or numeric string (e.g. `1.7`) | custom shadow factor | parsed value |
154
+
155
+ ```python
156
+ PrayTime().adjust({"asr": "Hanafi"})
157
+ ```
158
+
159
+ ## Options
160
+
161
+ | Setter | Values | Default | Notes |
162
+ |---|---|---|---|
163
+ | `.location([lat, lng])` | degrees | host-derived | latitude, longitude |
164
+ | `.timezone("...")` | IANA name | host zone | used when `utcOffset` is `"auto"` |
165
+ | `.adjust({...})` | any settings mapping | — | generic merge for `fajr`, `isha`, `dhuhr`, `maghrib`, `midnight`, `asr`, `highLats`, … |
166
+ | `.tune({...})` | prayer → minutes | `{}` | per-prayer offsets; fractions allowed |
167
+ | `.round(value)` | `"nearest"`, `"up"`, `"down"`, `"none"` | `"nearest"` | minute rounding (`nearest` is JS half-up) |
168
+ | `.format(value)` | `"24h"`, `"12h"`, `"12H"`, `"x"`, `"X"`, or a callable | `"24h"` | see formats below |
169
+ | `.utc_offset(value)` | number (minutes) or `"auto"` | `"auto"` | see the heuristic below |
170
+ | `.set({...})` | any settings mapping | — | low-level merge used by every setter |
171
+
172
+ ### Output formats
173
+
174
+ | `format` | Result (Asr example) |
175
+ |---|---|
176
+ | `"24h"` | `'15:32'` (zero-padded hour) |
177
+ | `"12h"` | `'3:32 PM'` (no leading zero) |
178
+ | `"12H"` | `'3:32'` (AM/PM stripped) |
179
+ | `"x"` | `1740169920000` (Unix milliseconds, `int`) |
180
+ | `"X"` | `1740169920` (Unix seconds, `int`) |
181
+ | callable | the callable's return value; it receives the rounded ms `int` |
182
+
183
+ ### `utc_offset` heuristic
184
+
185
+ `utc_offset(n)` treats a numeric `n` with `abs(n) < 16` as **hours** (`n * 60`),
186
+ and larger values as **minutes**. It also forces `timezone` to `"UTC"` and sets
187
+ `utcOffset` to the numeric value:
188
+
189
+ ```python
190
+ PrayTime().utc_offset(-5) # -300 minutes (UTC-5)
191
+ PrayTime().utc_offset(-4.5) # -270 minutes
192
+ PrayTime().utc_offset(-300) # -300 minutes (already minutes)
193
+ ```
194
+
195
+ A numeric `utcOffset` shifts the timestamp before rendering; `"auto"` renders in
196
+ the configured `timezone` instead. (For parity, calling `utc_offset("auto")`
197
+ forces `timezone="UTC"`.)
198
+
199
+ ### High-latitude rules
200
+
201
+ Controlled by `highLats` (default `"NightMiddle"`). They clamp Fajr/Isha/Maghrib
202
+ when the sun does not reach the required depression angle.
203
+
204
+ | `highLats` | Factor | Clamp |
205
+ |---|---|---|
206
+ | `"None"` | — | no adjustment (polar results may render as `'-----'`) |
207
+ | `"NightMiddle"` | 1/2 | Fajr = sunrise − night/2; Isha/Maghrib = sunset + night/2 |
208
+ | `"OneSeventh"` | 1/7 | Fajr = sunrise − night/7; Isha/Maghrib = sunset + night/7 |
209
+ | `"AngleBased"` | (1/60) × angle | proportional to the twilight angle |
210
+
211
+ where `night = 24 + sunrise − sunset`.
212
+
213
+ ## Backward-compatible `get_times`
214
+
215
+ ```python
216
+ praytime.get_times(date, location, timezone="auto", dst=0, format="24h")
217
+ ```
218
+
219
+ When `location` is omitted/falsy it delegates to `times(date)`. Otherwise it sets
220
+ the location, a numeric UTC offset (`timezone + dst`), and the format, then
221
+ computes. Note the third parameter is a **numeric UTC offset** (or `"auto"`),
222
+ matching the JS library — not an IANA zone name.
223
+
224
+ ## Command line
225
+
226
+ The package is runnable as `python -m praytime`, and installing it provides a
227
+ `praytime` console command:
228
+
229
+ ```bash
230
+ python -m praytime \
231
+ --method ISNA \
232
+ --lat 43.4643 --lng -80.5204 \
233
+ --timezone America/Toronto \
234
+ --date 2025-02-21
235
+ ```
236
+
237
+ ```text
238
+ Fajr 05:52
239
+ Sunrise 07:11
240
+ Dhuhr 12:36
241
+ Asr 15:32
242
+ Sunset 18:01
243
+ Maghrib 18:02
244
+ Isha 19:20
245
+ Midnight 00:36
246
+ ```
247
+
248
+ Options:
249
+
250
+ | Option | Default | Choices |
251
+ |---|---|---|
252
+ | `--method` | `MWL` | the 11 method names above |
253
+ | `--lat` | *(required)* | latitude in degrees |
254
+ | `--lng` | *(required)* | longitude in degrees |
255
+ | `--timezone` | `UTC` | IANA timezone name |
256
+ | `--date` | today | `YYYY-MM-DD` |
257
+ | `--format` | `24h` | `24h`, `12h`, `12H`, `x`, `X` |
258
+ | `--asr` | `Standard` | `Standard`, `Hanafi` |
259
+ | `--high-lats` | `NightMiddle` | `None`, `NightMiddle`, `OneSeventh`, `AngleBased` |
260
+
261
+ ## Compatibility & intentional deviations from the JS library
262
+
263
+ The port is numerically faithful (see the spec's §13.1 parity list). The
264
+ following differ intentionally, and only in ergonomics or error handling
265
+ (spec §13.2):
266
+
267
+ - **snake_case public methods.** `getTimes` → `get_times`, `utcOffset` →
268
+ `utc_offset`, `setMethod` → `set_method` (deprecated, returns `None`). The
269
+ class name stays `PrayTime`.
270
+ - **camelCase settings keys are preserved verbatim.** Keys consumed by the
271
+ algorithm and passed through `adjust({...})` — `highLats`, `utcOffset` — keep
272
+ their JS spelling so drop-in translations keep working.
273
+ - **Stricter invalid-date handling.** Malformed `(y, m, d)` tuples (wrong length)
274
+ and out-of-range dates raise `ValueError`; the JS `Date.UTC` silently
275
+ normalizes them. A wrong-length sequence raises
276
+ `ValueError("date must be (year, month, day)")`.
277
+ - **Type hints and private internals.** Full annotations are provided; the JS
278
+ instance fields `utcTime` / `adjusted` become the private `_utc_time` /
279
+ `_adjusted`. Observable behaviour is unchanged.
280
+ - **Per-instance method table.** `praytime.methods` is a deep copy, so mutating
281
+ one instance (e.g. adding `"Algeria"`) never leaks to another. The user
282
+ workflow `praytime.methods["X"] = {...}` is unchanged.
283
+ - **Unknown method names** apply only the shared `defaults` and do **not** raise,
284
+ matching the JS no-op.
285
+ - **Timezone rendering** uses `zoneinfo` (stdlib) rather than the host `Intl`
286
+ database; `12h` hours are rendered arithmetically (no leading zero) for
287
+ deterministic, portable output.
288
+ - **Preserved JS quirks** (not deviations, but worth knowing): the
289
+ `abs(n) < 16` hours heuristic in `utc_offset`, the `date < 1000` day-offset
290
+ heuristic in `times`, the stateful high-latitude `_adjusted` flag, and
291
+ `get_times`' numeric-offset third parameter.
292
+
293
+ The implementation specification lives in [`docs/specs.md`](docs/specs.md) and
294
+ the roadmap in [`docs/plan.md`](docs/plan.md).
295
+
296
+ ## Disclaimer
297
+
298
+ > [!WARNING]
299
+ > This library is a Python port of the original
300
+ > [`zarrabi/praytime`](https://github.com/zarrabi/praytime) library, produced
301
+ > with AI-agent code-generation tooling ("agent-driven development"). Although
302
+ > it is tested extensively against the upstream JavaScript implementation —
303
+ > including a pytest suite and thousands of golden snapshots generated directly
304
+ > from the original code — automated translation can still introduce bugs, so
305
+ > the prayer-time calculations may be incorrect in some cases.
306
+ >
307
+ > Always verify the computed times against an authoritative local source before
308
+ > relying on them for worship. This software is provided **"as is", without
309
+ > warranty of any kind**, and the authors and contributors accept no liability
310
+ > for its use — **you use it at your own responsibility.**
311
+
312
+ ## License
313
+
314
+ MIT — see [`LICENSE`](LICENSE). Preserves the upstream copyright notice:
315
+ `Copyright (c) 2007-2025 Hamid Zarrabi-Zadeh`.