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.
Files changed (43) hide show
  1. michraz-0.1.0/LICENSE +21 -0
  2. michraz-0.1.0/PKG-INFO +126 -0
  3. michraz-0.1.0/README.md +85 -0
  4. michraz-0.1.0/pyproject.toml +72 -0
  5. michraz-0.1.0/setup.cfg +4 -0
  6. michraz-0.1.0/src/michraz/__init__.py +34 -0
  7. michraz-0.1.0/src/michraz/__main__.py +3 -0
  8. michraz-0.1.0/src/michraz/_closeable.py +25 -0
  9. michraz-0.1.0/src/michraz/_dates.py +68 -0
  10. michraz-0.1.0/src/michraz/_enums.py +14 -0
  11. michraz-0.1.0/src/michraz/_http.py +253 -0
  12. michraz-0.1.0/src/michraz/_version.py +10 -0
  13. michraz-0.1.0/src/michraz/cli.py +146 -0
  14. michraz-0.1.0/src/michraz/client.py +116 -0
  15. michraz-0.1.0/src/michraz/errors.py +34 -0
  16. michraz-0.1.0/src/michraz/export.py +126 -0
  17. michraz-0.1.0/src/michraz/filters.py +111 -0
  18. michraz-0.1.0/src/michraz/models.py +92 -0
  19. michraz-0.1.0/src/michraz/py.typed +0 -0
  20. michraz-0.1.0/src/michraz/sources/__init__.py +7 -0
  21. michraz-0.1.0/src/michraz/sources/base.py +112 -0
  22. michraz-0.1.0/src/michraz/sources/beersheva.py +204 -0
  23. michraz-0.1.0/src/michraz/sources/data/rmi_settlements.json +1 -0
  24. michraz-0.1.0/src/michraz/sources/rmi.py +220 -0
  25. michraz-0.1.0/src/michraz.egg-info/PKG-INFO +126 -0
  26. michraz-0.1.0/src/michraz.egg-info/SOURCES.txt +41 -0
  27. michraz-0.1.0/src/michraz.egg-info/dependency_links.txt +1 -0
  28. michraz-0.1.0/src/michraz.egg-info/entry_points.txt +2 -0
  29. michraz-0.1.0/src/michraz.egg-info/requires.txt +17 -0
  30. michraz-0.1.0/src/michraz.egg-info/top_level.txt +1 -0
  31. michraz-0.1.0/tests/test_beersheva.py +175 -0
  32. michraz-0.1.0/tests/test_cli.py +109 -0
  33. michraz-0.1.0/tests/test_client.py +124 -0
  34. michraz-0.1.0/tests/test_dates.py +57 -0
  35. michraz-0.1.0/tests/test_e2e.py +69 -0
  36. michraz-0.1.0/tests/test_enums.py +82 -0
  37. michraz-0.1.0/tests/test_examples.py +18 -0
  38. michraz-0.1.0/tests/test_export.py +95 -0
  39. michraz-0.1.0/tests/test_filters.py +107 -0
  40. michraz-0.1.0/tests/test_http.py +250 -0
  41. michraz-0.1.0/tests/test_models.py +68 -0
  42. michraz-0.1.0/tests/test_package.py +5 -0
  43. 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
+ [![PyPI](https://img.shields.io/pypi/v/michraz)](https://pypi.org/project/michraz/)
45
+ [![Python](https://img.shields.io/pypi/pyversions/michraz)](https://pypi.org/project/michraz/)
46
+ [![CI](https://github.com/Zikithezikit/michraz/actions/workflows/ci.yml/badge.svg)](https://github.com/Zikithezikit/michraz/actions/workflows/ci.yml)
47
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ ![What michraz does](https://raw.githubusercontent.com/Zikithezikit/michraz/main/docs/images/overview.png)
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
+ ![Terminal output](https://raw.githubusercontent.com/Zikithezikit/michraz/main/docs/images/cli.png)
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
+ ![Classes](https://raw.githubusercontent.com/Zikithezikit/michraz/main/docs/images/classes.png)
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
+ ![Request flow](https://raw.githubusercontent.com/Zikithezikit/michraz/main/docs/images/request_flow.png)
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.
@@ -0,0 +1,85 @@
1
+ # michraz
2
+
3
+ [![PyPI](https://img.shields.io/pypi/v/michraz)](https://pypi.org/project/michraz/)
4
+ [![Python](https://img.shields.io/pypi/pyversions/michraz)](https://pypi.org/project/michraz/)
5
+ [![CI](https://github.com/Zikithezikit/michraz/actions/workflows/ci.yml/badge.svg)](https://github.com/Zikithezikit/michraz/actions/workflows/ci.yml)
6
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](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
+ ![What michraz does](https://raw.githubusercontent.com/Zikithezikit/michraz/main/docs/images/overview.png)
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
+ ![Terminal output](https://raw.githubusercontent.com/Zikithezikit/michraz/main/docs/images/cli.png)
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
+ ![Classes](https://raw.githubusercontent.com/Zikithezikit/michraz/main/docs/images/classes.png)
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
+ ![Request flow](https://raw.githubusercontent.com/Zikithezikit/michraz/main/docs/images/request_flow.png)
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
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -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,3 @@
1
+ from michraz.cli import main
2
+
3
+ raise SystemExit(main())
@@ -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)