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 +21 -0
- praytime-0.1.0/PKG-INFO +348 -0
- praytime-0.1.0/README.md +315 -0
- praytime-0.1.0/praytime/__init__.py +21 -0
- praytime-0.1.0/praytime/__main__.py +122 -0
- praytime-0.1.0/praytime/astro.py +114 -0
- praytime-0.1.0/praytime/constants.py +168 -0
- praytime-0.1.0/praytime/formatting.py +141 -0
- praytime-0.1.0/praytime/highlat.py +86 -0
- praytime-0.1.0/praytime/methods.py +41 -0
- praytime-0.1.0/praytime/praytime.py +292 -0
- praytime-0.1.0/praytime/py.typed +0 -0
- praytime-0.1.0/praytime/trig.py +102 -0
- praytime-0.1.0/praytime.egg-info/PKG-INFO +348 -0
- praytime-0.1.0/praytime.egg-info/SOURCES.txt +28 -0
- praytime-0.1.0/praytime.egg-info/dependency_links.txt +1 -0
- praytime-0.1.0/praytime.egg-info/entry_points.txt +2 -0
- praytime-0.1.0/praytime.egg-info/requires.txt +5 -0
- praytime-0.1.0/praytime.egg-info/top_level.txt +1 -0
- praytime-0.1.0/pyproject.toml +80 -0
- praytime-0.1.0/setup.cfg +4 -0
- praytime-0.1.0/tests/test_api.py +104 -0
- praytime-0.1.0/tests/test_astro.py +221 -0
- praytime-0.1.0/tests/test_date_inputs.py +123 -0
- praytime-0.1.0/tests/test_formatting.py +205 -0
- praytime-0.1.0/tests/test_foundations.py +215 -0
- praytime-0.1.0/tests/test_highlat.py +256 -0
- praytime-0.1.0/tests/test_methods.py +79 -0
- praytime-0.1.0/tests/test_reference.py +64 -0
- praytime-0.1.0/tests/test_snapshot.py +151 -0
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.
|
praytime-0.1.0/PKG-INFO
ADDED
|
@@ -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`.
|
praytime-0.1.0/README.md
ADDED
|
@@ -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`.
|