cnovatech-datamarket 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.
- cnovatech_datamarket-0.1.0/.gitignore +26 -0
- cnovatech_datamarket-0.1.0/CHANGELOG.md +49 -0
- cnovatech_datamarket-0.1.0/LICENSE +21 -0
- cnovatech_datamarket-0.1.0/PKG-INFO +235 -0
- cnovatech_datamarket-0.1.0/README.md +203 -0
- cnovatech_datamarket-0.1.0/pyproject.toml +98 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/__init__.py +20 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/_client.py +145 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/_config.py +24 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/_version.py +9 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/errors.py +117 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/models/__init__.py +168 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/models/account.py +186 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/models/brands.py +51 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/models/common.py +56 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/models/history.py +74 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/models/products.py +243 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/models/stores.py +322 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/py.typed +0 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/resources/__init__.py +24 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/resources/_operations.py +507 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/resources/_pricing.py +32 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/resources/_validation.py +28 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/resources/account.py +105 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/resources/brands.py +57 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/resources/products.py +343 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/resources/stores.py +475 -0
- cnovatech_datamarket-0.1.0/src/cnovatech/datamarket/types.py +37 -0
- cnovatech_datamarket-0.1.0/tests/conftest.py +135 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/README.md +31 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/checkCatalogCoverage.json +45 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/error_ean_not_found.json +7 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/error_filter_required.json +7 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/error_validation_error.json +12 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getCatalog.json +150 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getCompetitorProducts.json +107 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getProductByEan.json +142 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getProductPriceHistory.json +92 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getProductsByBrand.json +96 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getProfile.json +52 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getStoreAssortment.json +133 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getStoreCompetitiveness.json +59 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getStoreCompetitors.json +52 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getStoreMarketPosition.json +138 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getStoreOpportunities.json +130 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getStoresByBrand.json +108 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getUsage.json +31 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/getUserLogs.json +108 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/healthCheck.json +7 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/listApiKeys.json +49 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/listBrands.json +112 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/listDepartments.json +140 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/listStores.json +66 -0
- cnovatech_datamarket-0.1.0/tests/fixtures/openapi.json +1 -0
- cnovatech_datamarket-0.1.0/tests/test_account.py +110 -0
- cnovatech_datamarket-0.1.0/tests/test_brands.py +59 -0
- cnovatech_datamarket-0.1.0/tests/test_client.py +145 -0
- cnovatech_datamarket-0.1.0/tests/test_contract.py +52 -0
- cnovatech_datamarket-0.1.0/tests/test_find.py +123 -0
- cnovatech_datamarket-0.1.0/tests/test_live.py +44 -0
- cnovatech_datamarket-0.1.0/tests/test_models.py +112 -0
- cnovatech_datamarket-0.1.0/tests/test_operations.py +51 -0
- cnovatech_datamarket-0.1.0/tests/test_parity.py +105 -0
- cnovatech_datamarket-0.1.0/tests/test_pricing.py +34 -0
- cnovatech_datamarket-0.1.0/tests/test_products.py +310 -0
- cnovatech_datamarket-0.1.0/tests/test_secrets.py +45 -0
- cnovatech_datamarket-0.1.0/tests/test_stores.py +338 -0
- cnovatech_datamarket-0.1.0/tests/test_validation.py +35 -0
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Environments and secrets
|
|
2
|
+
.venv/
|
|
3
|
+
.venv-*/
|
|
4
|
+
.env
|
|
5
|
+
.env.*
|
|
6
|
+
!.env.example
|
|
7
|
+
|
|
8
|
+
# Build output
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
*.egg-info/
|
|
12
|
+
|
|
13
|
+
# Tool caches
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.mypy_cache/
|
|
16
|
+
.ruff_cache/
|
|
17
|
+
.coverage
|
|
18
|
+
coverage.xml
|
|
19
|
+
htmlcov/
|
|
20
|
+
__pycache__/
|
|
21
|
+
*.pyc
|
|
22
|
+
|
|
23
|
+
# Editors and OS
|
|
24
|
+
.idea/
|
|
25
|
+
.vscode/
|
|
26
|
+
.DS_Store
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `cnovatech-datamarket` are documented here. The format
|
|
4
|
+
follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and the
|
|
5
|
+
project uses [Semantic Versioning](https://semver.org/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0] - 2026-11-06
|
|
10
|
+
|
|
11
|
+
First release. Rehearsed on TestPyPI as 0.1.0.dev1.
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- `DataMarket` and `AsyncDataMarket` clients with configuration from arguments
|
|
16
|
+
or `DATAMARKET_API_KEY` / `DATAMARKET_BASE_URL`, connection pooling, context
|
|
17
|
+
manager support and a `repr` that masks the key. `max_retries` and
|
|
18
|
+
`max_units_per_request` are enforced by the shared core. A client can be
|
|
19
|
+
shared between threads; responses, pages and exceptions can be pickled.
|
|
20
|
+
- `health()` on both clients: unauthenticated, free of quota cost, and
|
|
21
|
+
tolerant of changes in the unversioned health body.
|
|
22
|
+
- Resources covering every read endpoint of the API: `products` (`catalog`,
|
|
23
|
+
`departments`, `brands`, `by_brand`, `get`, `price_history`, `coverage`),
|
|
24
|
+
`stores` (`list`, `find`, `assortment`, `competitors`, `competitiveness`,
|
|
25
|
+
`compare`, `market_position`, `opportunities`), `brands.stores` and
|
|
26
|
+
`account` (`profile`, `usage`, `logs`, `api_keys`). Each method states its
|
|
27
|
+
quota cost and estimates it before sending. `coverage()` takes any
|
|
28
|
+
iterable of codes; store identifiers may be `uuid.UUID` objects.
|
|
29
|
+
- Paginated responses (`CatalogPage`, `AssortmentPage`, ...) with
|
|
30
|
+
`next_page()`, `iter_pages()` and `iter_all()` capped by `max_pages` or
|
|
31
|
+
`max_units`, plus `estimate_remaining_cost()`.
|
|
32
|
+
- `stores.find(name)`: reads up to five pages of results until an exact name
|
|
33
|
+
matches; `StoreNotFoundError` and `AmbiguousStoreError` with the
|
|
34
|
+
candidates otherwise. A blank name is refused before anything is sent.
|
|
35
|
+
- `to_dataframe()` on every list response, with the `pandas` extra. An
|
|
36
|
+
envelope field that shares its name with a row field keeps its own column,
|
|
37
|
+
prefixed by the response's name (`product_image_url`).
|
|
38
|
+
- `client.usage` (units spent through the client, last balance, refreshed
|
|
39
|
+
by quota errors too) and `client.rate_limit` (per-minute limit from the
|
|
40
|
+
API's headers).
|
|
41
|
+
- `cnovatech.datamarket.errors` with `DataMarketError` as the root of every
|
|
42
|
+
exception the SDK raises, plus `EANNotFoundError`, `StoreNotFoundError`,
|
|
43
|
+
`FilterRequiredError`, `AmbiguousStoreError` and `BudgetExceededError`.
|
|
44
|
+
Connection errors and timeouts carry the `correlation_id` of the request.
|
|
45
|
+
- `cnovatech.datamarket.models`, one module per resource, and
|
|
46
|
+
`cnovatech.datamarket.types` with the input enumerations.
|
|
47
|
+
- `__version__`, read from the installed package metadata.
|
|
48
|
+
- Guides under `docs/`: quota and costs, pagination, errors and recipes.
|
|
49
|
+
- Support for Python 3.10 to 3.15.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Cnovatech
|
|
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,235 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: cnovatech-datamarket
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Official Python SDK for the Cnovatech Data Market API: price, availability and assortment intelligence for Brazilian supermarkets.
|
|
5
|
+
Project-URL: Homepage, https://datamarket.cnovatech.com.br
|
|
6
|
+
Project-URL: Documentation, https://datamarket.cnovatech.com.br/redoc
|
|
7
|
+
Project-URL: Repository, https://github.com/cnovatech-br/sdks
|
|
8
|
+
Project-URL: Changelog, https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/CHANGELOG.md
|
|
9
|
+
Author-email: Cnovatech <suporte@cnovatech.com.br>
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,assortment,cnovatech,data market,datamarket,ean,pricing,retail,sdk,supermarket
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: Natural Language :: English
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.15
|
|
24
|
+
Classifier: Topic :: Office/Business
|
|
25
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
26
|
+
Classifier: Typing :: Typed
|
|
27
|
+
Requires-Python: >=3.10
|
|
28
|
+
Requires-Dist: cnovatech-sdk-core<0.2,>=0.1.0
|
|
29
|
+
Provides-Extra: pandas
|
|
30
|
+
Requires-Dist: cnovatech-sdk-core[pandas]; extra == 'pandas'
|
|
31
|
+
Description-Content-Type: text/markdown
|
|
32
|
+
|
|
33
|
+
# cnovatech-datamarket
|
|
34
|
+
|
|
35
|
+
Official Python SDK for the [Cnovatech Data Market](https://datamarket.cnovatech.com.br)
|
|
36
|
+
API: daily prices, availability and assortment of more than 150,000 products
|
|
37
|
+
across 240+ online supermarkets in 23 Brazilian states.
|
|
38
|
+
|
|
39
|
+
- Synchronous and asynchronous clients with the same surface
|
|
40
|
+
- Every response carries its quota cost; pagination can be capped by cost
|
|
41
|
+
- Retries only where a repeat cannot be billed twice
|
|
42
|
+
- Typed end to end: pydantic models, `py.typed`, `mypy --strict`
|
|
43
|
+
|
|
44
|
+
> **Status: 0.x.** Every read endpoint of the API is implemented, validated
|
|
45
|
+
> against production and tested offline against recorded responses. Until 1.0
|
|
46
|
+
> a minor release may change the public surface; every such change is listed
|
|
47
|
+
> in the
|
|
48
|
+
> [changelog](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/CHANGELOG.md).
|
|
49
|
+
|
|
50
|
+
## Installation
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install cnovatech-datamarket
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Add `[pandas]` for `to_dataframe()` on list responses. Python 3.10 or newer.
|
|
57
|
+
|
|
58
|
+
## Quick start
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from cnovatech.datamarket import DataMarket
|
|
62
|
+
|
|
63
|
+
client = DataMarket() # reads DATAMARKET_API_KEY
|
|
64
|
+
|
|
65
|
+
product = client.products.get("7894900011517", state="SP")
|
|
66
|
+
|
|
67
|
+
product.product_name # "Refrigerante Coca-Cola 2L"
|
|
68
|
+
product.market.median_price # 9.49
|
|
69
|
+
product.offers[0].store_name # "Supermercado Modelo"
|
|
70
|
+
product.offers[0].price # 8.99
|
|
71
|
+
product.offers[0].available # True
|
|
72
|
+
|
|
73
|
+
product.usage.query_cost # 1
|
|
74
|
+
product.usage.remaining # 994
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Get a key at <https://datamarket.cnovatech.com.br>. `client.health()` checks
|
|
78
|
+
the API without a key and without cost.
|
|
79
|
+
|
|
80
|
+
The first lookup of a product the API has not cached yet can take up to 90
|
|
81
|
+
seconds; after that it answers in under a second. The default timeout allows
|
|
82
|
+
for it.
|
|
83
|
+
|
|
84
|
+
## Quota
|
|
85
|
+
|
|
86
|
+
Every call is billed in quota units, and every response says what it cost:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
page = client.products.catalog(search="azeite", limit=50)
|
|
90
|
+
page.usage.query_cost # 1
|
|
91
|
+
page.usage.remaining # 993
|
|
92
|
+
client.usage.spent # 2, through this client
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Paginated responses can be walked page by page or item by item, with a ceiling
|
|
96
|
+
on how much to spend:
|
|
97
|
+
|
|
98
|
+
```python
|
|
99
|
+
position = client.stores.market_position(store_id, limit=1000) # 8 units per page
|
|
100
|
+
position.meta.pages # 4
|
|
101
|
+
position.estimate_remaining_cost() # 24, for the three pages not fetched yet
|
|
102
|
+
|
|
103
|
+
items = list(position.iter_all(max_units=40))
|
|
104
|
+
items[0].product_name # "Azeite Extra Virgem 500ml"
|
|
105
|
+
items[0].price_position # "above"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
`iter_all` never requests again the page you already paid for. It stops quietly
|
|
109
|
+
at `max_pages` and raises `BudgetExceededError` before a page that would cross
|
|
110
|
+
`max_units`. To refuse any single call above a budget, build the client with
|
|
111
|
+
`DataMarket(max_units_per_request=20)`.
|
|
112
|
+
|
|
113
|
+
## Stores and competitors
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
store = client.stores.find("Supermercado Modelo", state="SP") # 1 unit
|
|
117
|
+
store.store_id # "0f8c7d2e-6b1a-4f3e-9c5d-2a7b8e1f4c63"
|
|
118
|
+
|
|
119
|
+
competitors = client.stores.competitors(store.store_id, limit=5) # 1 unit each
|
|
120
|
+
competitors.data[0].store_name # "Mercado Vizinho"
|
|
121
|
+
competitors.data[0].overlap_pct # 62.5, share of the assortment in common
|
|
122
|
+
|
|
123
|
+
comparison = client.stores.compare(store.store_id, competitors.data[0].store_id)
|
|
124
|
+
comparison.data[0].product_name # "Azeite Extra Virgem 500ml"
|
|
125
|
+
comparison.data[0].price_position # "above"
|
|
126
|
+
comparison.data[0].price_diff_pct # 12.3
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
## Errors
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
from cnovatech.datamarket import DataMarket, errors
|
|
133
|
+
|
|
134
|
+
client = DataMarket()
|
|
135
|
+
|
|
136
|
+
try:
|
|
137
|
+
product = client.products.get("0000000000000")
|
|
138
|
+
except errors.EANNotFoundError as exc:
|
|
139
|
+
exc.hint # what the API suggests doing next
|
|
140
|
+
except errors.QuotaExceededError as exc:
|
|
141
|
+
exc.code # "monthly_limit_exceeded"
|
|
142
|
+
exc.upgrade_url # where to change plan
|
|
143
|
+
except errors.APITimeoutError:
|
|
144
|
+
... # may have been billed; check client.account.usage() before repeating
|
|
145
|
+
except errors.DataMarketError as exc:
|
|
146
|
+
str(exc) # "HTTP 503 service_unavailable: ... [correlation_id=...]"; never the key
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Connection errors, 502/503 and the per-minute 429 are retried with backoff.
|
|
150
|
+
Read timeouts, 504 and quota errors never are: the request may already have
|
|
151
|
+
been processed and billed.
|
|
152
|
+
|
|
153
|
+
## Async
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
import asyncio
|
|
157
|
+
|
|
158
|
+
from cnovatech.datamarket import AsyncDataMarket
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
async def main() -> None:
|
|
162
|
+
async with AsyncDataMarket() as client:
|
|
163
|
+
product = await client.products.get("7894900011517")
|
|
164
|
+
product.market.median_price # 9.49
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
asyncio.run(main())
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## pandas
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
assortment = client.stores.assortment(store.store_id, limit=1000)
|
|
174
|
+
frame = assortment.to_dataframe() # one row per item; usage and meta as columns
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Configuration
|
|
178
|
+
|
|
179
|
+
| Argument | Environment variable | Default |
|
|
180
|
+
|---|---|---|
|
|
181
|
+
| `api_key` | `DATAMARKET_API_KEY` | required, except for `health()` |
|
|
182
|
+
| `base_url` | `DATAMARKET_BASE_URL` | `https://datamarket.cnovatech.com.br` |
|
|
183
|
+
| `timeout` | | 150 s read timeout; pass `httpx.Timeout` to set every phase. Applies per request, also through your own `http_client` |
|
|
184
|
+
| `max_retries` | | 2 |
|
|
185
|
+
| `max_units_per_request` | | no limit |
|
|
186
|
+
| `http_client` | | your own `httpx.Client`, for proxies, certificates or a shared pool; never closed, redirects never followed |
|
|
187
|
+
|
|
188
|
+
## Guides
|
|
189
|
+
|
|
190
|
+
- [Quota and costs](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/docs/quota-and-costs.md): the tariff, what each response reports, estimates and caps.
|
|
191
|
+
- [Pagination](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/docs/pagination.md): pages, walks, `max_pages` and `max_units`.
|
|
192
|
+
- [Errors](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/docs/errors.md): the hierarchy, what is retried, how to handle each case.
|
|
193
|
+
- [Recipes](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/docs/recipes.md): coverage of a list, competitors, exports to pandas, when to use `get` and when to export.
|
|
194
|
+
|
|
195
|
+
## Logging
|
|
196
|
+
|
|
197
|
+
The SDK logs through the standard `logging` module under the `cnovatech`
|
|
198
|
+
namespace and installs no handler of its own. At DEBUG it records each request's
|
|
199
|
+
method, path, status and duration; retries are logged at INFO, and a WARNING is
|
|
200
|
+
emitted when the remaining quota drops below 10%. Keys, headers and bodies are
|
|
201
|
+
never logged. The `httpx` logger, if the application enables it at INFO, prints
|
|
202
|
+
the full URL with its query string; the key is sent as a header and is not in it.
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
import logging
|
|
206
|
+
|
|
207
|
+
logging.basicConfig(level=logging.INFO)
|
|
208
|
+
logging.getLogger("cnovatech").setLevel(logging.DEBUG)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Resources
|
|
212
|
+
|
|
213
|
+
| Resource | Methods | Cost |
|
|
214
|
+
|---|---|---|
|
|
215
|
+
| `client.products` | `catalog`, `departments`, `brands`, `by_brand`, `get`, `price_history` | 1 unit |
|
|
216
|
+
| | `coverage` | ceil(codes / 250) units; up to 500 codes per call |
|
|
217
|
+
| `client.stores` | `list`, `find` | 1 unit |
|
|
218
|
+
| | `competitors`, `competitiveness` | 1 unit per competitor |
|
|
219
|
+
| | `assortment`, `compare`, `market_position`, `opportunities` | ceil(rows / 250) × 2 units per page |
|
|
220
|
+
| `client.brands` | `stores` | 1 unit |
|
|
221
|
+
| `client.account` | `profile`, `usage`, `logs`, `api_keys` | free |
|
|
222
|
+
| `client` | `health`, `usage`, `rate_limit`, `close` | free |
|
|
223
|
+
|
|
224
|
+
Each method's docstring states its cost. The API reference is at
|
|
225
|
+
<https://datamarket.cnovatech.com.br/redoc>.
|
|
226
|
+
|
|
227
|
+
## Support
|
|
228
|
+
|
|
229
|
+
- Issues and feature requests: <https://github.com/cnovatech-br/sdks/issues>
|
|
230
|
+
- Email: <suporte@cnovatech.com.br>
|
|
231
|
+
|
|
232
|
+
## License
|
|
233
|
+
|
|
234
|
+
[MIT](https://github.com/cnovatech-br/sdks/blob/main/LICENSE). The API terms
|
|
235
|
+
of use are at <https://cnovatech.com.br/termos-de-uso>.
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# cnovatech-datamarket
|
|
2
|
+
|
|
3
|
+
Official Python SDK for the [Cnovatech Data Market](https://datamarket.cnovatech.com.br)
|
|
4
|
+
API: daily prices, availability and assortment of more than 150,000 products
|
|
5
|
+
across 240+ online supermarkets in 23 Brazilian states.
|
|
6
|
+
|
|
7
|
+
- Synchronous and asynchronous clients with the same surface
|
|
8
|
+
- Every response carries its quota cost; pagination can be capped by cost
|
|
9
|
+
- Retries only where a repeat cannot be billed twice
|
|
10
|
+
- Typed end to end: pydantic models, `py.typed`, `mypy --strict`
|
|
11
|
+
|
|
12
|
+
> **Status: 0.x.** Every read endpoint of the API is implemented, validated
|
|
13
|
+
> against production and tested offline against recorded responses. Until 1.0
|
|
14
|
+
> a minor release may change the public surface; every such change is listed
|
|
15
|
+
> in the
|
|
16
|
+
> [changelog](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/CHANGELOG.md).
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install cnovatech-datamarket
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Add `[pandas]` for `to_dataframe()` on list responses. Python 3.10 or newer.
|
|
25
|
+
|
|
26
|
+
## Quick start
|
|
27
|
+
|
|
28
|
+
```python
|
|
29
|
+
from cnovatech.datamarket import DataMarket
|
|
30
|
+
|
|
31
|
+
client = DataMarket() # reads DATAMARKET_API_KEY
|
|
32
|
+
|
|
33
|
+
product = client.products.get("7894900011517", state="SP")
|
|
34
|
+
|
|
35
|
+
product.product_name # "Refrigerante Coca-Cola 2L"
|
|
36
|
+
product.market.median_price # 9.49
|
|
37
|
+
product.offers[0].store_name # "Supermercado Modelo"
|
|
38
|
+
product.offers[0].price # 8.99
|
|
39
|
+
product.offers[0].available # True
|
|
40
|
+
|
|
41
|
+
product.usage.query_cost # 1
|
|
42
|
+
product.usage.remaining # 994
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
Get a key at <https://datamarket.cnovatech.com.br>. `client.health()` checks
|
|
46
|
+
the API without a key and without cost.
|
|
47
|
+
|
|
48
|
+
The first lookup of a product the API has not cached yet can take up to 90
|
|
49
|
+
seconds; after that it answers in under a second. The default timeout allows
|
|
50
|
+
for it.
|
|
51
|
+
|
|
52
|
+
## Quota
|
|
53
|
+
|
|
54
|
+
Every call is billed in quota units, and every response says what it cost:
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
page = client.products.catalog(search="azeite", limit=50)
|
|
58
|
+
page.usage.query_cost # 1
|
|
59
|
+
page.usage.remaining # 993
|
|
60
|
+
client.usage.spent # 2, through this client
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
Paginated responses can be walked page by page or item by item, with a ceiling
|
|
64
|
+
on how much to spend:
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
position = client.stores.market_position(store_id, limit=1000) # 8 units per page
|
|
68
|
+
position.meta.pages # 4
|
|
69
|
+
position.estimate_remaining_cost() # 24, for the three pages not fetched yet
|
|
70
|
+
|
|
71
|
+
items = list(position.iter_all(max_units=40))
|
|
72
|
+
items[0].product_name # "Azeite Extra Virgem 500ml"
|
|
73
|
+
items[0].price_position # "above"
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`iter_all` never requests again the page you already paid for. It stops quietly
|
|
77
|
+
at `max_pages` and raises `BudgetExceededError` before a page that would cross
|
|
78
|
+
`max_units`. To refuse any single call above a budget, build the client with
|
|
79
|
+
`DataMarket(max_units_per_request=20)`.
|
|
80
|
+
|
|
81
|
+
## Stores and competitors
|
|
82
|
+
|
|
83
|
+
```python
|
|
84
|
+
store = client.stores.find("Supermercado Modelo", state="SP") # 1 unit
|
|
85
|
+
store.store_id # "0f8c7d2e-6b1a-4f3e-9c5d-2a7b8e1f4c63"
|
|
86
|
+
|
|
87
|
+
competitors = client.stores.competitors(store.store_id, limit=5) # 1 unit each
|
|
88
|
+
competitors.data[0].store_name # "Mercado Vizinho"
|
|
89
|
+
competitors.data[0].overlap_pct # 62.5, share of the assortment in common
|
|
90
|
+
|
|
91
|
+
comparison = client.stores.compare(store.store_id, competitors.data[0].store_id)
|
|
92
|
+
comparison.data[0].product_name # "Azeite Extra Virgem 500ml"
|
|
93
|
+
comparison.data[0].price_position # "above"
|
|
94
|
+
comparison.data[0].price_diff_pct # 12.3
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
## Errors
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from cnovatech.datamarket import DataMarket, errors
|
|
101
|
+
|
|
102
|
+
client = DataMarket()
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
product = client.products.get("0000000000000")
|
|
106
|
+
except errors.EANNotFoundError as exc:
|
|
107
|
+
exc.hint # what the API suggests doing next
|
|
108
|
+
except errors.QuotaExceededError as exc:
|
|
109
|
+
exc.code # "monthly_limit_exceeded"
|
|
110
|
+
exc.upgrade_url # where to change plan
|
|
111
|
+
except errors.APITimeoutError:
|
|
112
|
+
... # may have been billed; check client.account.usage() before repeating
|
|
113
|
+
except errors.DataMarketError as exc:
|
|
114
|
+
str(exc) # "HTTP 503 service_unavailable: ... [correlation_id=...]"; never the key
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
Connection errors, 502/503 and the per-minute 429 are retried with backoff.
|
|
118
|
+
Read timeouts, 504 and quota errors never are: the request may already have
|
|
119
|
+
been processed and billed.
|
|
120
|
+
|
|
121
|
+
## Async
|
|
122
|
+
|
|
123
|
+
```python
|
|
124
|
+
import asyncio
|
|
125
|
+
|
|
126
|
+
from cnovatech.datamarket import AsyncDataMarket
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
async def main() -> None:
|
|
130
|
+
async with AsyncDataMarket() as client:
|
|
131
|
+
product = await client.products.get("7894900011517")
|
|
132
|
+
product.market.median_price # 9.49
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
asyncio.run(main())
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## pandas
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
assortment = client.stores.assortment(store.store_id, limit=1000)
|
|
142
|
+
frame = assortment.to_dataframe() # one row per item; usage and meta as columns
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
## Configuration
|
|
146
|
+
|
|
147
|
+
| Argument | Environment variable | Default |
|
|
148
|
+
|---|---|---|
|
|
149
|
+
| `api_key` | `DATAMARKET_API_KEY` | required, except for `health()` |
|
|
150
|
+
| `base_url` | `DATAMARKET_BASE_URL` | `https://datamarket.cnovatech.com.br` |
|
|
151
|
+
| `timeout` | | 150 s read timeout; pass `httpx.Timeout` to set every phase. Applies per request, also through your own `http_client` |
|
|
152
|
+
| `max_retries` | | 2 |
|
|
153
|
+
| `max_units_per_request` | | no limit |
|
|
154
|
+
| `http_client` | | your own `httpx.Client`, for proxies, certificates or a shared pool; never closed, redirects never followed |
|
|
155
|
+
|
|
156
|
+
## Guides
|
|
157
|
+
|
|
158
|
+
- [Quota and costs](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/docs/quota-and-costs.md): the tariff, what each response reports, estimates and caps.
|
|
159
|
+
- [Pagination](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/docs/pagination.md): pages, walks, `max_pages` and `max_units`.
|
|
160
|
+
- [Errors](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/docs/errors.md): the hierarchy, what is retried, how to handle each case.
|
|
161
|
+
- [Recipes](https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/docs/recipes.md): coverage of a list, competitors, exports to pandas, when to use `get` and when to export.
|
|
162
|
+
|
|
163
|
+
## Logging
|
|
164
|
+
|
|
165
|
+
The SDK logs through the standard `logging` module under the `cnovatech`
|
|
166
|
+
namespace and installs no handler of its own. At DEBUG it records each request's
|
|
167
|
+
method, path, status and duration; retries are logged at INFO, and a WARNING is
|
|
168
|
+
emitted when the remaining quota drops below 10%. Keys, headers and bodies are
|
|
169
|
+
never logged. The `httpx` logger, if the application enables it at INFO, prints
|
|
170
|
+
the full URL with its query string; the key is sent as a header and is not in it.
|
|
171
|
+
|
|
172
|
+
```python
|
|
173
|
+
import logging
|
|
174
|
+
|
|
175
|
+
logging.basicConfig(level=logging.INFO)
|
|
176
|
+
logging.getLogger("cnovatech").setLevel(logging.DEBUG)
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
## Resources
|
|
180
|
+
|
|
181
|
+
| Resource | Methods | Cost |
|
|
182
|
+
|---|---|---|
|
|
183
|
+
| `client.products` | `catalog`, `departments`, `brands`, `by_brand`, `get`, `price_history` | 1 unit |
|
|
184
|
+
| | `coverage` | ceil(codes / 250) units; up to 500 codes per call |
|
|
185
|
+
| `client.stores` | `list`, `find` | 1 unit |
|
|
186
|
+
| | `competitors`, `competitiveness` | 1 unit per competitor |
|
|
187
|
+
| | `assortment`, `compare`, `market_position`, `opportunities` | ceil(rows / 250) × 2 units per page |
|
|
188
|
+
| `client.brands` | `stores` | 1 unit |
|
|
189
|
+
| `client.account` | `profile`, `usage`, `logs`, `api_keys` | free |
|
|
190
|
+
| `client` | `health`, `usage`, `rate_limit`, `close` | free |
|
|
191
|
+
|
|
192
|
+
Each method's docstring states its cost. The API reference is at
|
|
193
|
+
<https://datamarket.cnovatech.com.br/redoc>.
|
|
194
|
+
|
|
195
|
+
## Support
|
|
196
|
+
|
|
197
|
+
- Issues and feature requests: <https://github.com/cnovatech-br/sdks/issues>
|
|
198
|
+
- Email: <suporte@cnovatech.com.br>
|
|
199
|
+
|
|
200
|
+
## License
|
|
201
|
+
|
|
202
|
+
[MIT](https://github.com/cnovatech-br/sdks/blob/main/LICENSE). The API terms
|
|
203
|
+
of use are at <https://cnovatech.com.br/termos-de-uso>.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cnovatech-datamarket"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Official Python SDK for the Cnovatech Data Market API: price, availability and assortment intelligence for Brazilian supermarkets."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Cnovatech", email = "suporte@cnovatech.com.br" }]
|
|
10
|
+
keywords = [
|
|
11
|
+
"cnovatech", "datamarket", "data market", "pricing", "retail",
|
|
12
|
+
"supermarket", "assortment", "ean", "sdk", "api",
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Natural Language :: English",
|
|
18
|
+
"Operating System :: OS Independent",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
"Programming Language :: Python :: 3.14",
|
|
25
|
+
"Programming Language :: Python :: 3.15",
|
|
26
|
+
"Topic :: Office/Business",
|
|
27
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
28
|
+
"Typing :: Typed",
|
|
29
|
+
]
|
|
30
|
+
# The core and this package are released together; the pin follows the core's
|
|
31
|
+
# minor series and moves with every release.
|
|
32
|
+
dependencies = [
|
|
33
|
+
"cnovatech-sdk-core>=0.1.0,<0.2",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
[project.optional-dependencies]
|
|
37
|
+
pandas = ["cnovatech-sdk-core[pandas]"]
|
|
38
|
+
|
|
39
|
+
[project.urls]
|
|
40
|
+
Homepage = "https://datamarket.cnovatech.com.br"
|
|
41
|
+
Documentation = "https://datamarket.cnovatech.com.br/redoc"
|
|
42
|
+
Repository = "https://github.com/cnovatech-br/sdks"
|
|
43
|
+
Changelog = "https://github.com/cnovatech-br/sdks/blob/main/datamarket-python/CHANGELOG.md"
|
|
44
|
+
|
|
45
|
+
[tool.uv.sources]
|
|
46
|
+
cnovatech-sdk-core = { workspace = true }
|
|
47
|
+
|
|
48
|
+
[build-system]
|
|
49
|
+
requires = ["hatchling>=1.27,<2"]
|
|
50
|
+
build-backend = "hatchling.build"
|
|
51
|
+
|
|
52
|
+
# `src/cnovatech/` has no __init__.py on purpose: it is a PEP 420 namespace
|
|
53
|
+
# shared with `cnovatech-sdk-core` and the other product SDKs.
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/cnovatech"]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.build.targets.sdist]
|
|
58
|
+
include = ["src", "tests", "README.md", "CHANGELOG.md", "LICENSE", "pyproject.toml"]
|
|
59
|
+
|
|
60
|
+
[tool.mypy]
|
|
61
|
+
python_version = "3.10"
|
|
62
|
+
files = ["src", "tests"]
|
|
63
|
+
mypy_path = "src:../core/src:tests"
|
|
64
|
+
strict = true
|
|
65
|
+
warn_unreachable = true
|
|
66
|
+
namespace_packages = true
|
|
67
|
+
explicit_package_bases = true
|
|
68
|
+
plugins = ["pydantic.mypy"]
|
|
69
|
+
|
|
70
|
+
[tool.pydantic-mypy]
|
|
71
|
+
init_forbid_extra = true
|
|
72
|
+
init_typed = true
|
|
73
|
+
warn_required_dynamic_aliases = true
|
|
74
|
+
|
|
75
|
+
[tool.pytest.ini_options]
|
|
76
|
+
testpaths = ["tests"]
|
|
77
|
+
asyncio_mode = "auto"
|
|
78
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
79
|
+
addopts = "-q --strict-markers --strict-config"
|
|
80
|
+
markers = [
|
|
81
|
+
"live: calls the real API; needs --live and only touches free endpoints.",
|
|
82
|
+
]
|
|
83
|
+
filterwarnings = ["error"]
|
|
84
|
+
|
|
85
|
+
[tool.coverage.run]
|
|
86
|
+
source = ["src/cnovatech/datamarket"]
|
|
87
|
+
branch = true
|
|
88
|
+
|
|
89
|
+
[tool.coverage.report]
|
|
90
|
+
fail_under = 90
|
|
91
|
+
show_missing = true
|
|
92
|
+
exclude_also = [
|
|
93
|
+
"if TYPE_CHECKING:",
|
|
94
|
+
"if sys.version_info",
|
|
95
|
+
"@overload",
|
|
96
|
+
"raise NotImplementedError",
|
|
97
|
+
"\\.\\.\\.$",
|
|
98
|
+
]
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Official Python SDK for the Cnovatech Data Market API.
|
|
2
|
+
|
|
3
|
+
Typical use::
|
|
4
|
+
|
|
5
|
+
from cnovatech.datamarket import DataMarket
|
|
6
|
+
|
|
7
|
+
with DataMarket() as client: # reads DATAMARKET_API_KEY
|
|
8
|
+
product = client.products.get("7894900011517", state="SP")
|
|
9
|
+
print(product.market.median_price, product.usage.remaining)
|
|
10
|
+
|
|
11
|
+
``AsyncDataMarket`` offers the same surface with coroutines.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
from . import errors, models, types
|
|
17
|
+
from ._client import AsyncDataMarket, DataMarket
|
|
18
|
+
from ._version import __version__
|
|
19
|
+
|
|
20
|
+
__all__ = ["AsyncDataMarket", "DataMarket", "__version__", "errors", "models", "types"]
|