posokanei 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 (38) hide show
  1. posokanei-0.1.0/.gitignore +11 -0
  2. posokanei-0.1.0/CHANGELOG.md +27 -0
  3. posokanei-0.1.0/LICENSE +21 -0
  4. posokanei-0.1.0/Makefile +35 -0
  5. posokanei-0.1.0/PKG-INFO +183 -0
  6. posokanei-0.1.0/README.md +159 -0
  7. posokanei-0.1.0/pyproject.toml +83 -0
  8. posokanei-0.1.0/spec/openapi.yaml +792 -0
  9. posokanei-0.1.0/src/posokanei/__init__.py +58 -0
  10. posokanei-0.1.0/src/posokanei/_cache.py +49 -0
  11. posokanei-0.1.0/src/posokanei/_ratelimit.py +55 -0
  12. posokanei-0.1.0/src/posokanei/_request.py +275 -0
  13. posokanei-0.1.0/src/posokanei/aclient.py +247 -0
  14. posokanei-0.1.0/src/posokanei/client.py +244 -0
  15. posokanei-0.1.0/src/posokanei/errors.py +57 -0
  16. posokanei-0.1.0/src/posokanei/models.py +280 -0
  17. posokanei-0.1.0/src/posokanei/py.typed +0 -0
  18. posokanei-0.1.0/tests/__init__.py +0 -0
  19. posokanei-0.1.0/tests/conftest.py +58 -0
  20. posokanei-0.1.0/tests/contract.py +75 -0
  21. posokanei-0.1.0/tests/fixtures/README.md +21 -0
  22. posokanei-0.1.0/tests/fixtures/meta_categories_tree.json +1546 -0
  23. posokanei-0.1.0/tests/fixtures/meta_retailers.json +166 -0
  24. posokanei-0.1.0/tests/fixtures/meta_stats.json +32 -0
  25. posokanei-0.1.0/tests/fixtures/product_by_barcode.json +35 -0
  26. posokanei-0.1.0/tests/fixtures/product_competitors.json +181 -0
  27. posokanei-0.1.0/tests/fixtures/product_detail.json +143 -0
  28. posokanei-0.1.0/tests/fixtures/product_detail_2.json +168 -0
  29. posokanei-0.1.0/tests/fixtures/product_history.json +3101 -0
  30. posokanei-0.1.0/tests/fixtures/products_list.json +3010 -0
  31. posokanei-0.1.0/tests/fixtures/products_price_drop.json +362 -0
  32. posokanei-0.1.0/tests/fixtures/products_search.json +386 -0
  33. posokanei-0.1.0/tests/test_aclient.py +296 -0
  34. posokanei-0.1.0/tests/test_cache.py +33 -0
  35. posokanei-0.1.0/tests/test_client.py +399 -0
  36. posokanei-0.1.0/tests/test_contract.py +41 -0
  37. posokanei-0.1.0/tests/test_live.py +124 -0
  38. posokanei-0.1.0/tests/test_models.py +118 -0
@@ -0,0 +1,11 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.egg-info/
4
+ dist/
5
+ build/
6
+ .mypy_cache/
7
+ .ruff_cache/
8
+ .pytest_cache/
9
+ .coverage
10
+ htmlcov/
11
+ *.har
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ What changed for someone who uses the package. The format follows
4
+ [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and the versions follow
5
+ [Semantic Versioning](https://semver.org/spec/v2.0.0.html). Until 1.0 a minor version may change
6
+ the public API; a change in the upstream API that forces one is noted as such.
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-11
11
+
12
+ ### Added
13
+
14
+ - `PosoKanei` and `AsyncPosoKanei`, with `retailers`, `categories`, `products`, `iter_products`
15
+ (`aiter_products`), `product`, `search`, `by_barcode`, `price_drops`, `competitors` and `stats`.
16
+ - Pydantic v2 models for every response, generated from the spec. They keep fields they do not
17
+ know in `model_extra`.
18
+ - `spec/openapi.yaml`: an OpenAPI 3.1 description of the API, written from recorded responses.
19
+ - A rate limit (0.25 s between requests), an in-memory cache (five minutes) and retries on 429
20
+ and 5xx that honour `Retry-After`. A wait of more than 30 seconds is raised as
21
+ `RateLimitError` with `retry_after`, not slept on.
22
+ - Errors: `PosoKaneiError`, `APIError`, `NotFoundError`, `ForbiddenError`, `RateLimitError`,
23
+ `SchemaError`.
24
+ - Live tests (`make live`) that hold the real API against the spec with unknown fields forbidden.
25
+
26
+ [Unreleased]: https://github.com/kostaskonkk/python-posokanei/compare/v0.1.0...main
27
+ [0.1.0]: https://github.com/kostaskonkk/python-posokanei/releases/tag/v0.1.0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Kostas Konstantinidis
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,35 @@
1
+ .PHONY: models drift lint format type test live check
2
+
3
+ SPEC := spec/openapi.yaml
4
+ MODELS := src/posokanei/models.py
5
+
6
+ # The models are generated from the spec and committed. Never edit them by hand.
7
+ models:
8
+ datamodel-codegen --input $(SPEC) --input-file-type openapi --output $(MODELS) \
9
+ --output-model-type pydantic_v2.BaseModel --target-python-version 3.10 \
10
+ --use-annotated --use-standard-collections --use-union-operator --field-constraints \
11
+ --extra-fields allow --use-schema-description --disable-timestamp \
12
+ --output-datetime-class datetime --formatters ruff-check ruff-format
13
+
14
+ drift: models
15
+ git diff --exit-code -- $(MODELS)
16
+
17
+ lint:
18
+ ruff check .
19
+ ruff format --check .
20
+
21
+ format:
22
+ ruff check --fix .
23
+ ruff format .
24
+
25
+ type:
26
+ mypy
27
+
28
+ test:
29
+ pytest --cov --cov-report=term-missing
30
+
31
+ # Calls the real API. Add ARGS=--record to write the responses to tests/fixtures/.
32
+ live:
33
+ pytest -m live $(ARGS)
34
+
35
+ check: lint type test
@@ -0,0 +1,183 @@
1
+ Metadata-Version: 2.5
2
+ Name: posokanei
3
+ Version: 0.1.0
4
+ Summary: Unofficial typed Python client for the price API behind posokanei.gov.gr
5
+ Project-URL: Homepage, https://github.com/kostaskonkk/python-posokanei
6
+ Project-URL: Issues, https://github.com/kostaskonkk/python-posokanei/issues
7
+ Project-URL: Changelog, https://github.com/kostaskonkk/python-posokanei/blob/main/CHANGELOG.md
8
+ Author: Kostas Konstantinidis
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,greece,posokanei,prices,pydantic,supermarket
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Typing :: Typed
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pydantic>=2.6
23
+ Description-Content-Type: text/markdown
24
+
25
+ # posokanei
26
+
27
+ **Unofficial** Python client for the price API behind [posokanei.gov.gr](https://posokanei.gov.gr),
28
+ the Greek government's supermarket price observatory. It is not affiliated with posokanei.gov.gr
29
+ or with the Greek Ministry of Development.
30
+
31
+ The platform publishes no API documentation. This package gives you typed
32
+ [Pydantic](https://docs.pydantic.dev) models, a sync and an async client, and a hand-written
33
+ [OpenAPI 3.1 description of the API](spec/openapi.yaml) that you can use from any language.
34
+
35
+ ```
36
+ pip install posokanei
37
+ ```
38
+
39
+ Python 3.10 or newer.
40
+
41
+ ## The official site
42
+
43
+ [posokanei.gov.gr](https://posokanei.gov.gr) is the platform's own interface, and the place to go
44
+ if you just want to look up a price: browse the categories, search a product, compare its price
45
+ across supermarkets and countries, see its price history and the current price drops. This
46
+ package reads the same API that site does, so what you get here is what you see there.
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ from posokanei import PosoKanei
52
+
53
+ with PosoKanei() as pk:
54
+ greek = pk.retailers(country="GR")
55
+ print([retailer.id for retailer in greek]) # 'sklavenitis', 'ab_vasilopoulos', 'lidl', ...
56
+
57
+ tree = pk.categories()
58
+ category = tree.tree[0].children[0].children[0] # a category's id is what products() takes
59
+
60
+ page = pk.products(category.category_id, sort_by="unit_price", sort_order="asc")
61
+ cheapest = page.products[0]
62
+ print(cheapest.name, cheapest.price_stats.min_unit_price, "EUR per", cheapest.unit)
63
+
64
+ product = pk.product(cheapest.id, history=True)
65
+ for price in product.retailer_prices:
66
+ print(price.retailer, price.price)
67
+ for retailer, days in product.history.daily_prices.items():
68
+ print(retailer, days[0].date, days[0].price, "->", days[-1].date, days[-1].price)
69
+ ```
70
+
71
+ ## What it covers
72
+
73
+ | Method | Returns | Request |
74
+ |---|---|---|
75
+ | `retailers(country="GR")` | `list[Retailer]` | `GET /meta/retailers` |
76
+ | `categories(include_counts=True, include_hidden=False)` | `CategoryTree` | `GET /meta/categories/tree` |
77
+ | `products(category, page=1, page_size=36, ...)` | `ProductPage` | `GET /products` |
78
+ | `iter_products(category, ...)` | every `Product`, page after page | `GET /products` |
79
+ | `product(id, history=False, ...)` | `Product` | `GET /products/{id}` |
80
+ | `search(title, page=1, page_size=36, ...)` | `ProductPage` | `POST /products/search` |
81
+ | `by_barcode(ean)` | `Product` or `None` | `GET /products/barcode/{ean}` |
82
+ | `price_drops()` | `list[PriceDrop]` | `GET /products/price-drop` |
83
+ | `competitors(id, retailer=...)` | `Competitors` | `GET /products/{id}/competitors` |
84
+ | `stats()` | `Stats` | `GET /meta/stats` |
85
+
86
+ `AsyncPosoKanei` has the same methods with `await`, and `aiter_products` for `iter_products`:
87
+
88
+ ```python
89
+ async with AsyncPosoKanei() as pk:
90
+ page = await pk.search("γάλα")
91
+ ```
92
+
93
+ `country` is `"all"` or an ISO country code. A price is in EUR; `price_normalized` and the unit
94
+ prices are EUR per `unit` of the product (`kg`, `L`, ...). Timestamps have no offset and are UTC.
95
+
96
+ Things the API does that you might not expect:
97
+
98
+ - `price_drops()` is not paged: one call returns every active price drop, about 1,800 items.
99
+ - `/meta/retailers` returns every country's retailers whatever you ask for, so `retailers()`
100
+ picks the country's on your side.
101
+ - `competitors()` needs a retailer id, and gives the other products of the same category with
102
+ their price at that retailer.
103
+ - A product no retailer has a price for has `price_stats = None` and empty lists.
104
+ - The barcode endpoint has a much tighter rate limit than the rest: after a handful of requests
105
+ it answers 429 and asks for a wait of about a quarter of an hour.
106
+
107
+ Search, barcode, competitors and stats each rest on a single recorded response, so their models
108
+ are the least certain. That a barcode nobody knows gives a 404, and so `None`, is reported by
109
+ posokanei-mcp and has not been seen here.
110
+
111
+ ## Errors
112
+
113
+ Everything the package raises itself derives from `PosoKaneiError`:
114
+
115
+ | Error | When |
116
+ |---|---|
117
+ | `NotFoundError` | 404 |
118
+ | `ForbiddenError` | 403, see below |
119
+ | `RateLimitError` | 429, still, after the retries |
120
+ | `APIError` | any other 4xx or 5xx, or a body that is not JSON. Has `status_code` and `body` |
121
+ | `SchemaError` | the answer does not fit its model. Has `payload`, what the API sent |
122
+
123
+ A network failure stays an `httpx` exception.
124
+
125
+ The models accept fields they do not know (they end up in `model_extra`), so a field added
126
+ upstream does not break your code. A field that goes missing or changes type raises `SchemaError`.
127
+
128
+ ## Be polite
129
+
130
+ This is a public service, so the defaults are careful, and please keep them that way:
131
+
132
+ - at least 0.25 seconds between two requests (`min_interval`),
133
+ - an answer is kept in memory for five minutes (`cache_ttl`; 0 turns it off),
134
+ - a 429 or a 5xx is tried again up to three times with a growing wait, and a `Retry-After` is
135
+ honoured. When the API asks for more than half a minute, the client does not sit it out: it
136
+ raises `RateLimitError` at once, with the wait in `retry_after`.
137
+
138
+ The prices belong to the platform. If you want history in bulk, use the dataset at
139
+ [spyrosavl/posokanei](https://github.com/spyrosavl/posokanei) instead of crawling the API.
140
+
141
+ ## If you get a 403
142
+
143
+ The API refuses requests that do not look like its web app's. Measured on 2026-10-11:
144
+
145
+ - The `User-Agent` has to be a browser's. The client sends one by default (`user_agent=`), along
146
+ with the web app's `Origin`, `Referer`, `X-Platform` and `X-App-Version`.
147
+ - It also refuses some clients whatever their headers. Python's default TLS settings were
148
+ refused; the same request with OpenSSL's default cipher list was served. The client therefore
149
+ builds its connection that way (certificates are checked as usual). If you pass an
150
+ `http_client` of your own, give it `verify=posokanei._request.ssl_context()`.
151
+
152
+ None of this is documented by the platform and it can change without notice.
153
+
154
+ ## The API description
155
+
156
+ [`spec/openapi.yaml`](spec/openapi.yaml) is the source of truth of this package: the models in
157
+ `src/posokanei/models.py` are generated from it. It stands on its own as documentation of the
158
+ API, written from the recorded responses in [`tests/fixtures/`](tests/fixtures).
159
+
160
+ ## Development
161
+
162
+ ```
163
+ uv sync
164
+ uv run make check # ruff, mypy --strict, the tests
165
+ uv run make models # after a change to spec/openapi.yaml; never edit models.py by hand
166
+ uv run make live # calls the real API, about ten requests
167
+ ```
168
+
169
+ `make live` holds every answer against the spec with unknown fields forbidden. That is how a
170
+ change upstream is noticed: run it when something looks off, or on a schedule.
171
+ `make live ARGS=--record` also writes the answers to `tests/fixtures/`.
172
+
173
+ How to change the spec, add an endpoint or report that the API changed is in
174
+ [CONTRIBUTING.md](CONTRIBUTING.md); what changed between versions is in [CHANGELOG.md](CHANGELOG.md).
175
+
176
+ ## Credits
177
+
178
+ The search, barcode, stats and competitors endpoints were found by
179
+ [charistas/posokanei-mcp](https://github.com/charistas/posokanei-mcp) (MIT).
180
+
181
+ ## License
182
+
183
+ MIT
@@ -0,0 +1,159 @@
1
+ # posokanei
2
+
3
+ **Unofficial** Python client for the price API behind [posokanei.gov.gr](https://posokanei.gov.gr),
4
+ the Greek government's supermarket price observatory. It is not affiliated with posokanei.gov.gr
5
+ or with the Greek Ministry of Development.
6
+
7
+ The platform publishes no API documentation. This package gives you typed
8
+ [Pydantic](https://docs.pydantic.dev) models, a sync and an async client, and a hand-written
9
+ [OpenAPI 3.1 description of the API](spec/openapi.yaml) that you can use from any language.
10
+
11
+ ```
12
+ pip install posokanei
13
+ ```
14
+
15
+ Python 3.10 or newer.
16
+
17
+ ## The official site
18
+
19
+ [posokanei.gov.gr](https://posokanei.gov.gr) is the platform's own interface, and the place to go
20
+ if you just want to look up a price: browse the categories, search a product, compare its price
21
+ across supermarkets and countries, see its price history and the current price drops. This
22
+ package reads the same API that site does, so what you get here is what you see there.
23
+
24
+ ## Quickstart
25
+
26
+ ```python
27
+ from posokanei import PosoKanei
28
+
29
+ with PosoKanei() as pk:
30
+ greek = pk.retailers(country="GR")
31
+ print([retailer.id for retailer in greek]) # 'sklavenitis', 'ab_vasilopoulos', 'lidl', ...
32
+
33
+ tree = pk.categories()
34
+ category = tree.tree[0].children[0].children[0] # a category's id is what products() takes
35
+
36
+ page = pk.products(category.category_id, sort_by="unit_price", sort_order="asc")
37
+ cheapest = page.products[0]
38
+ print(cheapest.name, cheapest.price_stats.min_unit_price, "EUR per", cheapest.unit)
39
+
40
+ product = pk.product(cheapest.id, history=True)
41
+ for price in product.retailer_prices:
42
+ print(price.retailer, price.price)
43
+ for retailer, days in product.history.daily_prices.items():
44
+ print(retailer, days[0].date, days[0].price, "->", days[-1].date, days[-1].price)
45
+ ```
46
+
47
+ ## What it covers
48
+
49
+ | Method | Returns | Request |
50
+ |---|---|---|
51
+ | `retailers(country="GR")` | `list[Retailer]` | `GET /meta/retailers` |
52
+ | `categories(include_counts=True, include_hidden=False)` | `CategoryTree` | `GET /meta/categories/tree` |
53
+ | `products(category, page=1, page_size=36, ...)` | `ProductPage` | `GET /products` |
54
+ | `iter_products(category, ...)` | every `Product`, page after page | `GET /products` |
55
+ | `product(id, history=False, ...)` | `Product` | `GET /products/{id}` |
56
+ | `search(title, page=1, page_size=36, ...)` | `ProductPage` | `POST /products/search` |
57
+ | `by_barcode(ean)` | `Product` or `None` | `GET /products/barcode/{ean}` |
58
+ | `price_drops()` | `list[PriceDrop]` | `GET /products/price-drop` |
59
+ | `competitors(id, retailer=...)` | `Competitors` | `GET /products/{id}/competitors` |
60
+ | `stats()` | `Stats` | `GET /meta/stats` |
61
+
62
+ `AsyncPosoKanei` has the same methods with `await`, and `aiter_products` for `iter_products`:
63
+
64
+ ```python
65
+ async with AsyncPosoKanei() as pk:
66
+ page = await pk.search("γάλα")
67
+ ```
68
+
69
+ `country` is `"all"` or an ISO country code. A price is in EUR; `price_normalized` and the unit
70
+ prices are EUR per `unit` of the product (`kg`, `L`, ...). Timestamps have no offset and are UTC.
71
+
72
+ Things the API does that you might not expect:
73
+
74
+ - `price_drops()` is not paged: one call returns every active price drop, about 1,800 items.
75
+ - `/meta/retailers` returns every country's retailers whatever you ask for, so `retailers()`
76
+ picks the country's on your side.
77
+ - `competitors()` needs a retailer id, and gives the other products of the same category with
78
+ their price at that retailer.
79
+ - A product no retailer has a price for has `price_stats = None` and empty lists.
80
+ - The barcode endpoint has a much tighter rate limit than the rest: after a handful of requests
81
+ it answers 429 and asks for a wait of about a quarter of an hour.
82
+
83
+ Search, barcode, competitors and stats each rest on a single recorded response, so their models
84
+ are the least certain. That a barcode nobody knows gives a 404, and so `None`, is reported by
85
+ posokanei-mcp and has not been seen here.
86
+
87
+ ## Errors
88
+
89
+ Everything the package raises itself derives from `PosoKaneiError`:
90
+
91
+ | Error | When |
92
+ |---|---|
93
+ | `NotFoundError` | 404 |
94
+ | `ForbiddenError` | 403, see below |
95
+ | `RateLimitError` | 429, still, after the retries |
96
+ | `APIError` | any other 4xx or 5xx, or a body that is not JSON. Has `status_code` and `body` |
97
+ | `SchemaError` | the answer does not fit its model. Has `payload`, what the API sent |
98
+
99
+ A network failure stays an `httpx` exception.
100
+
101
+ The models accept fields they do not know (they end up in `model_extra`), so a field added
102
+ upstream does not break your code. A field that goes missing or changes type raises `SchemaError`.
103
+
104
+ ## Be polite
105
+
106
+ This is a public service, so the defaults are careful, and please keep them that way:
107
+
108
+ - at least 0.25 seconds between two requests (`min_interval`),
109
+ - an answer is kept in memory for five minutes (`cache_ttl`; 0 turns it off),
110
+ - a 429 or a 5xx is tried again up to three times with a growing wait, and a `Retry-After` is
111
+ honoured. When the API asks for more than half a minute, the client does not sit it out: it
112
+ raises `RateLimitError` at once, with the wait in `retry_after`.
113
+
114
+ The prices belong to the platform. If you want history in bulk, use the dataset at
115
+ [spyrosavl/posokanei](https://github.com/spyrosavl/posokanei) instead of crawling the API.
116
+
117
+ ## If you get a 403
118
+
119
+ The API refuses requests that do not look like its web app's. Measured on 2026-10-11:
120
+
121
+ - The `User-Agent` has to be a browser's. The client sends one by default (`user_agent=`), along
122
+ with the web app's `Origin`, `Referer`, `X-Platform` and `X-App-Version`.
123
+ - It also refuses some clients whatever their headers. Python's default TLS settings were
124
+ refused; the same request with OpenSSL's default cipher list was served. The client therefore
125
+ builds its connection that way (certificates are checked as usual). If you pass an
126
+ `http_client` of your own, give it `verify=posokanei._request.ssl_context()`.
127
+
128
+ None of this is documented by the platform and it can change without notice.
129
+
130
+ ## The API description
131
+
132
+ [`spec/openapi.yaml`](spec/openapi.yaml) is the source of truth of this package: the models in
133
+ `src/posokanei/models.py` are generated from it. It stands on its own as documentation of the
134
+ API, written from the recorded responses in [`tests/fixtures/`](tests/fixtures).
135
+
136
+ ## Development
137
+
138
+ ```
139
+ uv sync
140
+ uv run make check # ruff, mypy --strict, the tests
141
+ uv run make models # after a change to spec/openapi.yaml; never edit models.py by hand
142
+ uv run make live # calls the real API, about ten requests
143
+ ```
144
+
145
+ `make live` holds every answer against the spec with unknown fields forbidden. That is how a
146
+ change upstream is noticed: run it when something looks off, or on a schedule.
147
+ `make live ARGS=--record` also writes the answers to `tests/fixtures/`.
148
+
149
+ How to change the spec, add an endpoint or report that the API changed is in
150
+ [CONTRIBUTING.md](CONTRIBUTING.md); what changed between versions is in [CHANGELOG.md](CHANGELOG.md).
151
+
152
+ ## Credits
153
+
154
+ The search, barcode, stats and competitors endpoints were found by
155
+ [charistas/posokanei-mcp](https://github.com/charistas/posokanei-mcp) (MIT).
156
+
157
+ ## License
158
+
159
+ MIT
@@ -0,0 +1,83 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "posokanei"
7
+ dynamic = ["version"]
8
+ description = "Unofficial typed Python client for the price API behind posokanei.gov.gr"
9
+ readme = "README.md"
10
+ license = "MIT"
11
+ license-files = ["LICENSE"]
12
+ requires-python = ">=3.10"
13
+ authors = [{ name = "Kostas Konstantinidis" }]
14
+ keywords = ["posokanei", "greece", "supermarket", "prices", "api", "pydantic"]
15
+ classifiers = [
16
+ "Development Status :: 3 - Alpha",
17
+ "Intended Audience :: Developers",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.10",
20
+ "Programming Language :: Python :: 3.11",
21
+ "Programming Language :: Python :: 3.12",
22
+ "Programming Language :: Python :: 3.13",
23
+ "Typing :: Typed",
24
+ ]
25
+ dependencies = ["httpx>=0.27", "pydantic>=2.6"]
26
+
27
+ [project.urls]
28
+ Homepage = "https://github.com/kostaskonkk/python-posokanei"
29
+ Issues = "https://github.com/kostaskonkk/python-posokanei/issues"
30
+ Changelog = "https://github.com/kostaskonkk/python-posokanei/blob/main/CHANGELOG.md"
31
+
32
+ [dependency-groups]
33
+ dev = [
34
+ "datamodel-code-generator==0.83.0", # pinned: the drift check compares its output
35
+ "jsonschema>=4.21",
36
+ "mypy>=1.11",
37
+ "pytest>=8",
38
+ "pytest-asyncio>=0.24",
39
+ "pytest-cov>=5",
40
+ "pyyaml>=6",
41
+ "respx>=0.21",
42
+ "ruff==0.17.0", # pinned: it formats the generated models
43
+ "types-jsonschema",
44
+ "types-pyyaml",
45
+ ]
46
+
47
+ [tool.hatch.version]
48
+ path = "src/posokanei/__init__.py"
49
+
50
+ [tool.hatch.build.targets.wheel]
51
+ packages = ["src/posokanei"]
52
+
53
+ [tool.hatch.build.targets.sdist]
54
+ include = ["src", "spec", "tests", "README.md", "CHANGELOG.md", "LICENSE", "Makefile"]
55
+
56
+ [tool.ruff]
57
+ line-length = 100
58
+ target-version = "py310"
59
+
60
+ [tool.ruff.lint]
61
+ select = ["E", "F", "I", "B", "UP", "SIM", "RUF", "A", "C4", "PT"]
62
+
63
+ [tool.ruff.lint.per-file-ignores]
64
+ "src/posokanei/models.py" = ["E501"] # generated; a description is as long as the spec's
65
+
66
+ [tool.mypy]
67
+ strict = true
68
+ plugins = ["pydantic.mypy"]
69
+ files = ["src", "tests"]
70
+
71
+ [tool.pytest.ini_options]
72
+ testpaths = ["tests"]
73
+ asyncio_mode = "auto"
74
+ addopts = "-m 'not live'"
75
+ markers = ["live: calls the real API; run by hand with `pytest -m live`"]
76
+
77
+ [tool.coverage.run]
78
+ source = ["posokanei"]
79
+ omit = ["*/posokanei/models.py"]
80
+
81
+ [tool.coverage.report]
82
+ fail_under = 90
83
+ show_missing = true