michraz 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.
- michraz-0.1.0/LICENSE +21 -0
- michraz-0.1.0/PKG-INFO +126 -0
- michraz-0.1.0/README.md +85 -0
- michraz-0.1.0/pyproject.toml +72 -0
- michraz-0.1.0/setup.cfg +4 -0
- michraz-0.1.0/src/michraz/__init__.py +34 -0
- michraz-0.1.0/src/michraz/__main__.py +3 -0
- michraz-0.1.0/src/michraz/_closeable.py +25 -0
- michraz-0.1.0/src/michraz/_dates.py +68 -0
- michraz-0.1.0/src/michraz/_enums.py +14 -0
- michraz-0.1.0/src/michraz/_http.py +253 -0
- michraz-0.1.0/src/michraz/_version.py +10 -0
- michraz-0.1.0/src/michraz/cli.py +146 -0
- michraz-0.1.0/src/michraz/client.py +116 -0
- michraz-0.1.0/src/michraz/errors.py +34 -0
- michraz-0.1.0/src/michraz/export.py +126 -0
- michraz-0.1.0/src/michraz/filters.py +111 -0
- michraz-0.1.0/src/michraz/models.py +92 -0
- michraz-0.1.0/src/michraz/py.typed +0 -0
- michraz-0.1.0/src/michraz/sources/__init__.py +7 -0
- michraz-0.1.0/src/michraz/sources/base.py +112 -0
- michraz-0.1.0/src/michraz/sources/beersheva.py +204 -0
- michraz-0.1.0/src/michraz/sources/data/rmi_settlements.json +1 -0
- michraz-0.1.0/src/michraz/sources/rmi.py +220 -0
- michraz-0.1.0/src/michraz.egg-info/PKG-INFO +126 -0
- michraz-0.1.0/src/michraz.egg-info/SOURCES.txt +41 -0
- michraz-0.1.0/src/michraz.egg-info/dependency_links.txt +1 -0
- michraz-0.1.0/src/michraz.egg-info/entry_points.txt +2 -0
- michraz-0.1.0/src/michraz.egg-info/requires.txt +17 -0
- michraz-0.1.0/src/michraz.egg-info/top_level.txt +1 -0
- michraz-0.1.0/tests/test_beersheva.py +175 -0
- michraz-0.1.0/tests/test_cli.py +109 -0
- michraz-0.1.0/tests/test_client.py +124 -0
- michraz-0.1.0/tests/test_dates.py +57 -0
- michraz-0.1.0/tests/test_e2e.py +69 -0
- michraz-0.1.0/tests/test_enums.py +82 -0
- michraz-0.1.0/tests/test_examples.py +18 -0
- michraz-0.1.0/tests/test_export.py +95 -0
- michraz-0.1.0/tests/test_filters.py +107 -0
- michraz-0.1.0/tests/test_http.py +250 -0
- michraz-0.1.0/tests/test_models.py +68 -0
- michraz-0.1.0/tests/test_package.py +5 -0
- michraz-0.1.0/tests/test_rmi.py +176 -0
michraz-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Yoav Haimov
|
|
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.
|
michraz-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: michraz
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: One clean interface to Israeli public tenders.
|
|
5
|
+
Author-email: Yoav Haimov <haimovyoav.work@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Zikithezikit/michraz
|
|
8
|
+
Project-URL: Documentation, https://github.com/Zikithezikit/michraz#readme
|
|
9
|
+
Project-URL: Issues, https://github.com/Zikithezikit/michraz/issues
|
|
10
|
+
Project-URL: Changelog, https://github.com/Zikithezikit/michraz/blob/main/CHANGELOG.md
|
|
11
|
+
Project-URL: PyPI, https://pypi.org/project/michraz/
|
|
12
|
+
Keywords: israel,tenders,procurement,michraz,open-data
|
|
13
|
+
Classifier: Development Status :: 3 - Alpha
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Operating System :: OS Independent
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries
|
|
17
|
+
Classifier: Typing :: Typed
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
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: Programming Language :: Python :: 3.14
|
|
24
|
+
Requires-Python: >=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: httpx>=0.27
|
|
28
|
+
Requires-Dist: tzdata; sys_platform == "win32"
|
|
29
|
+
Provides-Extra: dev
|
|
30
|
+
Requires-Dist: pytest; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-cov; extra == "dev"
|
|
32
|
+
Requires-Dist: ruff; extra == "dev"
|
|
33
|
+
Requires-Dist: mypy; extra == "dev"
|
|
34
|
+
Requires-Dist: build; extra == "dev"
|
|
35
|
+
Requires-Dist: twine; extra == "dev"
|
|
36
|
+
Requires-Dist: respx; extra == "dev"
|
|
37
|
+
Requires-Dist: pandas; extra == "dev"
|
|
38
|
+
Provides-Extra: pandas
|
|
39
|
+
Requires-Dist: pandas; extra == "pandas"
|
|
40
|
+
Dynamic: license-file
|
|
41
|
+
|
|
42
|
+
# michraz
|
|
43
|
+
|
|
44
|
+
[](https://pypi.org/project/michraz/)
|
|
45
|
+
[](https://pypi.org/project/michraz/)
|
|
46
|
+
[](https://github.com/Zikithezikit/michraz/actions/workflows/ci.yml)
|
|
47
|
+
[](https://github.com/Zikithezikit/michraz/blob/main/LICENSE)
|
|
48
|
+
|
|
49
|
+
Israeli public tenders (מכרזים) in Python. One `Tender` object, whichever site it came from.
|
|
50
|
+
|
|
51
|
+
Sources today: Israel Land Authority land tenders and Beer Sheva municipal tenders.
|
|
52
|
+
|
|
53
|
+
> Unofficial client. It can lag or miss tenders, and parsing can break when a source changes. Confirm deadlines on
|
|
54
|
+
> the publisher's page before you act. Not affiliated with the Israel Land Authority or any municipality.
|
|
55
|
+
|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
## Install
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pip install michraz # Python 3.10+
|
|
62
|
+
pip install "michraz[pandas]" # optional, for DataFrames
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
## Use it
|
|
66
|
+
|
|
67
|
+
```python
|
|
68
|
+
from michraz import Michraz, to_csv
|
|
69
|
+
|
|
70
|
+
with Michraz() as m:
|
|
71
|
+
tenders = m.search(keyword="גינון", open_only=True)
|
|
72
|
+
|
|
73
|
+
for t in tenders:
|
|
74
|
+
print(f"{t.deadline:%Y-%m-%d} {t.title} {t.url}")
|
|
75
|
+
|
|
76
|
+
to_csv(tenders, "tenders.csv")
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Results are sorted by deadline. Filter by `source`, `closing_before` and `closing_after` (dates or aware
|
|
80
|
+
datetimes) too. Each tender has an `attribution` string. Keep it when you republish the data.
|
|
81
|
+
|
|
82
|
+
Or from the terminal:
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
michraz search --open --source rmi
|
|
86
|
+
michraz search --keyword גינון --csv gardening.csv
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
A record that fails to parse is skipped and listed in `m.last_errors`. An unreachable source raises
|
|
92
|
+
`SourceUnavailableError`. More scripts are in [examples](https://github.com/Zikithezikit/michraz/tree/main/examples).
|
|
93
|
+
|
|
94
|
+
## Add a source
|
|
95
|
+
|
|
96
|
+
Write a class with `name`, `description`, `attribution`, `errors` and a `fetch()` that yields `Tender`s, then pass
|
|
97
|
+
it to `Michraz([MySource()])`.
|
|
98
|
+
|
|
99
|
+

|
|
100
|
+
|
|
101
|
+
Fields are documented in [docs/model.md](https://github.com/Zikithezikit/michraz/blob/main/docs/model.md), and how
|
|
102
|
+
each source was checked in [docs/sources.md](https://github.com/Zikithezikit/michraz/blob/main/docs/sources.md).
|
|
103
|
+
|
|
104
|
+
## Polite by default
|
|
105
|
+
|
|
106
|
+

|
|
107
|
+
|
|
108
|
+
Honest User-Agent, one request per second per host, one-hour disk cache, at most two retries. Tune it with
|
|
109
|
+
`Michraz(cache_dir=None, cache_ttl=..., min_interval=...)`.
|
|
110
|
+
|
|
111
|
+
## Limitations
|
|
112
|
+
|
|
113
|
+
- RMI region, type, purpose and status are raw numeric codes in `tender.extra`. No verified label table exists.
|
|
114
|
+
- RMI tender details and documents are not fetched yet.
|
|
115
|
+
- Keyword search is a Unicode-normalized substring match. No stemming.
|
|
116
|
+
|
|
117
|
+
## Development
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
make venv && make check
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Tests run against recorded responses and never touch the network. See
|
|
124
|
+
[CONTRIBUTING.md](https://github.com/Zikithezikit/michraz/blob/main/CONTRIBUTING.md).
|
|
125
|
+
|
|
126
|
+
MIT licensed. The data belongs to its publishers and carries their licenses.
|
michraz-0.1.0/README.md
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# michraz
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/michraz/)
|
|
4
|
+
[](https://pypi.org/project/michraz/)
|
|
5
|
+
[](https://github.com/Zikithezikit/michraz/actions/workflows/ci.yml)
|
|
6
|
+
[](https://github.com/Zikithezikit/michraz/blob/main/LICENSE)
|
|
7
|
+
|
|
8
|
+
Israeli public tenders (מכרזים) in Python. One `Tender` object, whichever site it came from.
|
|
9
|
+
|
|
10
|
+
Sources today: Israel Land Authority land tenders and Beer Sheva municipal tenders.
|
|
11
|
+
|
|
12
|
+
> Unofficial client. It can lag or miss tenders, and parsing can break when a source changes. Confirm deadlines on
|
|
13
|
+
> the publisher's page before you act. Not affiliated with the Israel Land Authority or any municipality.
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install michraz # Python 3.10+
|
|
21
|
+
pip install "michraz[pandas]" # optional, for DataFrames
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Use it
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
from michraz import Michraz, to_csv
|
|
28
|
+
|
|
29
|
+
with Michraz() as m:
|
|
30
|
+
tenders = m.search(keyword="גינון", open_only=True)
|
|
31
|
+
|
|
32
|
+
for t in tenders:
|
|
33
|
+
print(f"{t.deadline:%Y-%m-%d} {t.title} {t.url}")
|
|
34
|
+
|
|
35
|
+
to_csv(tenders, "tenders.csv")
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Results are sorted by deadline. Filter by `source`, `closing_before` and `closing_after` (dates or aware
|
|
39
|
+
datetimes) too. Each tender has an `attribution` string. Keep it when you republish the data.
|
|
40
|
+
|
|
41
|
+
Or from the terminal:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
michraz search --open --source rmi
|
|
45
|
+
michraz search --keyword גינון --csv gardening.csv
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
A record that fails to parse is skipped and listed in `m.last_errors`. An unreachable source raises
|
|
51
|
+
`SourceUnavailableError`. More scripts are in [examples](https://github.com/Zikithezikit/michraz/tree/main/examples).
|
|
52
|
+
|
|
53
|
+
## Add a source
|
|
54
|
+
|
|
55
|
+
Write a class with `name`, `description`, `attribution`, `errors` and a `fetch()` that yields `Tender`s, then pass
|
|
56
|
+
it to `Michraz([MySource()])`.
|
|
57
|
+
|
|
58
|
+

|
|
59
|
+
|
|
60
|
+
Fields are documented in [docs/model.md](https://github.com/Zikithezikit/michraz/blob/main/docs/model.md), and how
|
|
61
|
+
each source was checked in [docs/sources.md](https://github.com/Zikithezikit/michraz/blob/main/docs/sources.md).
|
|
62
|
+
|
|
63
|
+
## Polite by default
|
|
64
|
+
|
|
65
|
+

|
|
66
|
+
|
|
67
|
+
Honest User-Agent, one request per second per host, one-hour disk cache, at most two retries. Tune it with
|
|
68
|
+
`Michraz(cache_dir=None, cache_ttl=..., min_interval=...)`.
|
|
69
|
+
|
|
70
|
+
## Limitations
|
|
71
|
+
|
|
72
|
+
- RMI region, type, purpose and status are raw numeric codes in `tender.extra`. No verified label table exists.
|
|
73
|
+
- RMI tender details and documents are not fetched yet.
|
|
74
|
+
- Keyword search is a Unicode-normalized substring match. No stemming.
|
|
75
|
+
|
|
76
|
+
## Development
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
make venv && make check
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
Tests run against recorded responses and never touch the network. See
|
|
83
|
+
[CONTRIBUTING.md](https://github.com/Zikithezikit/michraz/blob/main/CONTRIBUTING.md).
|
|
84
|
+
|
|
85
|
+
MIT licensed. The data belongs to its publishers and carries their licenses.
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=77"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "michraz"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "One clean interface to Israeli public tenders."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = "MIT"
|
|
12
|
+
license-files = ["LICENSE"]
|
|
13
|
+
authors = [{ name = "Yoav Haimov", email = "haimovyoav.work@gmail.com" }]
|
|
14
|
+
keywords = ["israel", "tenders", "procurement", "michraz", "open-data"]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Topic :: Software Development :: Libraries",
|
|
20
|
+
"Typing :: Typed",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Programming Language :: Python :: 3.13",
|
|
26
|
+
"Programming Language :: Python :: 3.14",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
dependencies = [
|
|
30
|
+
"httpx>=0.27",
|
|
31
|
+
"tzdata; sys_platform == 'win32'",
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
[project.urls]
|
|
35
|
+
Homepage = "https://github.com/Zikithezikit/michraz"
|
|
36
|
+
Documentation = "https://github.com/Zikithezikit/michraz#readme"
|
|
37
|
+
Issues = "https://github.com/Zikithezikit/michraz/issues"
|
|
38
|
+
Changelog = "https://github.com/Zikithezikit/michraz/blob/main/CHANGELOG.md"
|
|
39
|
+
PyPI = "https://pypi.org/project/michraz/"
|
|
40
|
+
|
|
41
|
+
[project.optional-dependencies]
|
|
42
|
+
dev = ["pytest", "pytest-cov", "ruff", "mypy", "build", "twine", "respx", "pandas"]
|
|
43
|
+
pandas = ["pandas"]
|
|
44
|
+
|
|
45
|
+
[project.scripts]
|
|
46
|
+
michraz = "michraz.cli:main"
|
|
47
|
+
|
|
48
|
+
[tool.setuptools.package-data]
|
|
49
|
+
michraz = ["py.typed", "sources/data/*.json"]
|
|
50
|
+
|
|
51
|
+
[tool.pytest.ini_options]
|
|
52
|
+
testpaths = ["tests"]
|
|
53
|
+
addopts = "-m 'not live'"
|
|
54
|
+
markers = ["live: talks to the real services; run with `pytest -m live`"]
|
|
55
|
+
|
|
56
|
+
[tool.ruff]
|
|
57
|
+
line-length = 120
|
|
58
|
+
src = ["src"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff.lint]
|
|
61
|
+
select = ["E", "F", "I", "UP", "B"]
|
|
62
|
+
|
|
63
|
+
[tool.ruff.lint.per-file-ignores]
|
|
64
|
+
"tests/*" = ["E501"]
|
|
65
|
+
|
|
66
|
+
[tool.mypy]
|
|
67
|
+
files = ["src"]
|
|
68
|
+
strict = true
|
|
69
|
+
|
|
70
|
+
[[tool.mypy.overrides]]
|
|
71
|
+
module = "pandas"
|
|
72
|
+
ignore_missing_imports = true
|
michraz-0.1.0/setup.cfg
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"""michraz: one clean interface to Israeli public tenders.
|
|
2
|
+
|
|
3
|
+
from michraz import Michraz
|
|
4
|
+
|
|
5
|
+
with Michraz() as m:
|
|
6
|
+
for tender in m.search(keyword="גינון", open_only=True):
|
|
7
|
+
print(tender.deadline, tender.title)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from michraz._version import __version__
|
|
11
|
+
from michraz.client import Michraz
|
|
12
|
+
from michraz.errors import MichrazError, SourceFormatError, SourceUnavailableError
|
|
13
|
+
from michraz.export import to_csv, to_dataframe, to_dicts
|
|
14
|
+
from michraz.models import Document, Tender
|
|
15
|
+
from michraz.sources import BeerShevaExtra, BeerShevaSource, RmiExtra, RmiSource, Source, SourceName
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"BeerShevaExtra",
|
|
19
|
+
"BeerShevaSource",
|
|
20
|
+
"Document",
|
|
21
|
+
"Michraz",
|
|
22
|
+
"MichrazError",
|
|
23
|
+
"RmiExtra",
|
|
24
|
+
"RmiSource",
|
|
25
|
+
"Source",
|
|
26
|
+
"SourceName",
|
|
27
|
+
"SourceFormatError",
|
|
28
|
+
"SourceUnavailableError",
|
|
29
|
+
"Tender",
|
|
30
|
+
"__version__",
|
|
31
|
+
"to_csv",
|
|
32
|
+
"to_dataframe",
|
|
33
|
+
"to_dicts",
|
|
34
|
+
]
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""Context-manager behaviour shared by every object that owns a connection pool."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import TypeVar
|
|
8
|
+
|
|
9
|
+
_T = TypeVar("_T", bound="Closeable")
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Closeable(ABC):
|
|
13
|
+
"""An object with a :meth:`close` method that can be used in a ``with`` block."""
|
|
14
|
+
|
|
15
|
+
@abstractmethod
|
|
16
|
+
def close(self) -> None:
|
|
17
|
+
"""Releases the resources this object owns."""
|
|
18
|
+
|
|
19
|
+
def __enter__(self: _T) -> _T:
|
|
20
|
+
return self
|
|
21
|
+
|
|
22
|
+
def __exit__(
|
|
23
|
+
self, exc_type: type[BaseException] | None, exc: BaseException | None, tb: TracebackType | None
|
|
24
|
+
) -> None:
|
|
25
|
+
self.close()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Date parsing helpers. Every datetime michraz produces is timezone-aware."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from zoneinfo import ZoneInfo
|
|
8
|
+
|
|
9
|
+
TIMEZONE_NAME = "Asia/Jerusalem"
|
|
10
|
+
JERUSALEM = ZoneInfo(TIMEZONE_NAME)
|
|
11
|
+
|
|
12
|
+
LOCAL_FORMAT = "%d/%m/%Y %H:%M:%S"
|
|
13
|
+
|
|
14
|
+
UTC_DESIGNATOR = "Z"
|
|
15
|
+
UTC_OFFSET = "+00:00"
|
|
16
|
+
|
|
17
|
+
_FRACTION = re.compile(r"\.(\d+)")
|
|
18
|
+
MICROSECOND_DIGITS = 6
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _pad_fraction(match: re.Match[str]) -> str:
|
|
22
|
+
# Python 3.10 only accepts fractions of exactly 3 or 6 digits; the RMI API sends 2 ("57.55").
|
|
23
|
+
return "." + match.group(1)[:MICROSECOND_DIGITS].ljust(MICROSECOND_DIGITS, "0")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def parse_iso(value: str | None) -> datetime | None:
|
|
27
|
+
"""Parses an ISO 8601 timestamp and converts it to Asia/Jerusalem.
|
|
28
|
+
|
|
29
|
+
Args:
|
|
30
|
+
value: Timestamp such as ``"2026-12-28T12:00:00+02:00"``. A value without an offset is taken as
|
|
31
|
+
Asia/Jerusalem local time.
|
|
32
|
+
|
|
33
|
+
Returns:
|
|
34
|
+
An aware datetime, or None if ``value`` is None or blank.
|
|
35
|
+
|
|
36
|
+
Raises:
|
|
37
|
+
ValueError: If ``value`` is not a valid ISO 8601 timestamp.
|
|
38
|
+
"""
|
|
39
|
+
if value is None or not value.strip():
|
|
40
|
+
return None
|
|
41
|
+
text = _FRACTION.sub(_pad_fraction, value.strip().replace(UTC_DESIGNATOR, UTC_OFFSET))
|
|
42
|
+
parsed = datetime.fromisoformat(text)
|
|
43
|
+
if parsed.tzinfo is None:
|
|
44
|
+
return parsed.replace(tzinfo=JERUSALEM)
|
|
45
|
+
return parsed.astimezone(JERUSALEM)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def parse_local(value: str | None, fmt: str = LOCAL_FORMAT) -> datetime | None:
|
|
49
|
+
"""Parses a local Israeli timestamp such as ``"10/11/2026 12:00:00"``.
|
|
50
|
+
|
|
51
|
+
Args:
|
|
52
|
+
value: Text to parse.
|
|
53
|
+
fmt: ``strptime`` format. Defaults to ``dd/mm/yyyy HH:MM:SS``.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
An aware datetime in Asia/Jerusalem, or None if ``value`` is None or blank.
|
|
57
|
+
|
|
58
|
+
Raises:
|
|
59
|
+
ValueError: If ``value`` does not match ``fmt``.
|
|
60
|
+
"""
|
|
61
|
+
if value is None or not value.strip():
|
|
62
|
+
return None
|
|
63
|
+
return datetime.strptime(value.strip(), fmt).replace(tzinfo=JERUSALEM)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def now() -> datetime:
|
|
67
|
+
"""Returns the current time in Asia/Jerusalem."""
|
|
68
|
+
return datetime.now(JERUSALEM)
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
"""Enum base used across the package."""
|
|
2
|
+
|
|
3
|
+
from enum import Enum
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class StrEnum(str, Enum):
|
|
7
|
+
"""A string enum whose members equal, hash and print as their values.
|
|
8
|
+
|
|
9
|
+
``StrEnum.MEMBER == "value"`` is true and members work as dictionary keys next to plain strings, so code
|
|
10
|
+
that still passes the raw string keeps working. ``enum.StrEnum`` exists only from Python 3.11.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __str__(self) -> str:
|
|
14
|
+
return str(self.value)
|