tiny-erp-py 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 (48) hide show
  1. tiny_erp_py-0.1.0/.claude/settings.json +26 -0
  2. tiny_erp_py-0.1.0/.gitignore +179 -0
  3. tiny_erp_py-0.1.0/CLAUDE.md +355 -0
  4. tiny_erp_py-0.1.0/PKG-INFO +109 -0
  5. tiny_erp_py-0.1.0/README.md +81 -0
  6. tiny_erp_py-0.1.0/docs/.nojekyll +0 -0
  7. tiny_erp_py-0.1.0/docs/README.md +41 -0
  8. tiny_erp_py-0.1.0/docs/_sidebar.md +22 -0
  9. tiny_erp_py-0.1.0/docs/client.md +88 -0
  10. tiny_erp_py-0.1.0/docs/exceptions.md +113 -0
  11. tiny_erp_py-0.1.0/docs/guides/async.md +92 -0
  12. tiny_erp_py-0.1.0/docs/guides/fastapi.md +110 -0
  13. tiny_erp_py-0.1.0/docs/guides/rabbitmq.md +104 -0
  14. tiny_erp_py-0.1.0/docs/index.html +97 -0
  15. tiny_erp_py-0.1.0/docs/installation.md +39 -0
  16. tiny_erp_py-0.1.0/docs/models/order.md +69 -0
  17. tiny_erp_py-0.1.0/docs/models/product.md +117 -0
  18. tiny_erp_py-0.1.0/docs/quickstart.md +146 -0
  19. tiny_erp_py-0.1.0/docs/rate-limiting.md +70 -0
  20. tiny_erp_py-0.1.0/docs/resources/orders.md +72 -0
  21. tiny_erp_py-0.1.0/docs/resources/products.md +128 -0
  22. tiny_erp_py-0.1.0/docs/roadmap.md +254 -0
  23. tiny_erp_py-0.1.0/pyproject.toml +49 -0
  24. tiny_erp_py-0.1.0/tests/__init__.py +0 -0
  25. tiny_erp_py-0.1.0/tests/conftest.py +0 -0
  26. tiny_erp_py-0.1.0/tests/test_client.py +227 -0
  27. tiny_erp_py-0.1.0/tests/test_exceptions.py +70 -0
  28. tiny_erp_py-0.1.0/tests/test_http.py +310 -0
  29. tiny_erp_py-0.1.0/tests/test_models.py +318 -0
  30. tiny_erp_py-0.1.0/tests/test_rate_limiter.py +160 -0
  31. tiny_erp_py-0.1.0/tests/test_resources.py +398 -0
  32. tiny_erp_py-0.1.0/tiny_py/__init__.py +5 -0
  33. tiny_erp_py-0.1.0/tiny_py/_async_http.py +135 -0
  34. tiny_erp_py-0.1.0/tiny_py/_client.py +105 -0
  35. tiny_erp_py-0.1.0/tiny_py/_http.py +160 -0
  36. tiny_erp_py-0.1.0/tiny_py/_rate_limiter.py +65 -0
  37. tiny_erp_py-0.1.0/tiny_py/exceptions.py +27 -0
  38. tiny_erp_py-0.1.0/tiny_py/models/__init__.py +25 -0
  39. tiny_erp_py-0.1.0/tiny_py/models/order.py +41 -0
  40. tiny_erp_py-0.1.0/tiny_py/models/product.py +61 -0
  41. tiny_erp_py-0.1.0/tiny_py/py.typed +0 -0
  42. tiny_erp_py-0.1.0/tiny_py/resources/__init__.py +4 -0
  43. tiny_erp_py-0.1.0/tiny_py/resources/_async_base.py +35 -0
  44. tiny_erp_py-0.1.0/tiny_py/resources/_base.py +35 -0
  45. tiny_erp_py-0.1.0/tiny_py/resources/async_orders.py +30 -0
  46. tiny_erp_py-0.1.0/tiny_py/resources/async_products.py +57 -0
  47. tiny_erp_py-0.1.0/tiny_py/resources/orders.py +31 -0
  48. tiny_erp_py-0.1.0/tiny_py/resources/products.py +58 -0
@@ -0,0 +1,26 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Bash(pip install:*)",
5
+ "Bash(python -m pytest tests/ -v)",
6
+ "Bash(python -m pytest tests/test_exceptions.py -v)",
7
+ "Bash(python -m pytest tests/test_rate_limiter.py -v)",
8
+ "Bash(python -m pytest tests/test_models.py -v)",
9
+ "Bash(python -m pytest tests/test_http.py -v)",
10
+ "Bash(python -m pytest tests/test_resources.py -v)",
11
+ "Bash(python -m pytest tests/test_client.py -v)",
12
+ "Bash(pip show:*)",
13
+ "Bash(curl -s https://pypi.org/pypi/tiny-py/json)",
14
+ "Bash(python -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(''''EXISTS:'''', d[''''info''''][''''version'''']\\)\")",
15
+ "Bash(curl -s -o /dev/null -w \"%{http_code}\" https://pypi.org/pypi/tiny-py/json)",
16
+ "Bash(pytest tests/ -q)",
17
+ "Bash(python -m build)",
18
+ "Bash(python -m twine check dist/*)",
19
+ "Bash(TWINE_USERNAME=Joaoaalves TWINE_PASSWORD='mz.@T6?hMJ46!~.' python -m twine upload dist/*)",
20
+ "Bash(TWINE_USERNAME=__token__ TWINE_PASSWORD='pypi-AgEIcHlwaS5vcmcCJGFlMDcwODgwLWVmODctNDk3MC1hYzIxLTY0ODBhMWVmNGU4ZQACKlszLCJmNjVmMTJiYi00MmZkLTQ4OTYtODNlMi0zNjM4NmJkZjVkNTkiXQAABiCUVbejFSVH8tsk5GtzvitQvNVHs2dbHmXxZRscrZ3Niw' python -m twine upload dist/*)",
21
+ "Bash(TWINE_USERNAME=__token__ TWINE_PASSWORD='pypi-AgEIcHlwaS5vcmcCJGFlMDcwODgwLWVmODctNDk3MC1hYzIxLTY0ODBhMWVmNGU4ZQACKlszLCJmNjVmMTJiYi00MmZkLTQ4OTYtODNlMi0zNjM4NmJkZjVkNTkiXQAABiCUVbejFSVH8tsk5GtzvitQvNVHs2dbHmXxZRscrZ3Niw' python -m twine upload dist/* --verbose)",
22
+ "Bash(rm -rf dist/)",
23
+ "Bash(python -m build -q)"
24
+ ]
25
+ }
26
+ }
@@ -0,0 +1,179 @@
1
+ # Created by https://www.toptal.com/developers/gitignore/api/python
2
+ # Edit at https://www.toptal.com/developers/gitignore?templates=python
3
+
4
+ # IF SOMEONE USES AI
5
+ */CLAUDE.md
6
+
7
+ ### Python ###
8
+ # Byte-compiled / optimized / DLL files
9
+ __pycache__/
10
+ *.py[cod]
11
+ *$py.class
12
+
13
+ # C extensions
14
+ *.so
15
+
16
+ # Distribution / packaging
17
+ .Python
18
+ build/
19
+ develop-eggs/
20
+ dist/
21
+ downloads/
22
+ eggs/
23
+ .eggs/
24
+ lib/
25
+ lib64/
26
+ parts/
27
+ sdist/
28
+ var/
29
+ wheels/
30
+ share/python-wheels/
31
+ *.egg-info/
32
+ .installed.cfg
33
+ *.egg
34
+ MANIFEST
35
+
36
+ # PyInstaller
37
+ # Usually these files are written by a python script from a template
38
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
39
+ *.manifest
40
+ *.spec
41
+
42
+ # Installer logs
43
+ pip-log.txt
44
+ pip-delete-this-directory.txt
45
+
46
+ # Unit test / coverage reports
47
+ htmlcov/
48
+ .tox/
49
+ .nox/
50
+ .coverage
51
+ .coverage.*
52
+ .cache
53
+ nosetests.xml
54
+ coverage.xml
55
+ *.cover
56
+ *.py,cover
57
+ .hypothesis/
58
+ .pytest_cache/
59
+ cover/
60
+
61
+ # Translations
62
+ *.mo
63
+ *.pot
64
+
65
+ # Django stuff:
66
+ *.log
67
+ local_settings.py
68
+ db.sqlite3
69
+ db.sqlite3-journal
70
+
71
+ # Flask stuff:
72
+ instance/
73
+ .webassets-cache
74
+
75
+ # Scrapy stuff:
76
+ .scrapy
77
+
78
+ # Sphinx documentation
79
+ docs/_build/
80
+
81
+ # PyBuilder
82
+ .pybuilder/
83
+ target/
84
+
85
+ # Jupyter Notebook
86
+ .ipynb_checkpoints
87
+
88
+ # IPython
89
+ profile_default/
90
+ ipython_config.py
91
+
92
+ # pyenv
93
+ # For a library or package, you might want to ignore these files since the code is
94
+ # intended to run in multiple environments; otherwise, check them in:
95
+ # .python-version
96
+
97
+ # pipenv
98
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
99
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
100
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
101
+ # install all needed dependencies.
102
+ #Pipfile.lock
103
+
104
+ # poetry
105
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
106
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
107
+ # commonly ignored for libraries.
108
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
109
+ #poetry.lock
110
+
111
+ # pdm
112
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
113
+ #pdm.lock
114
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
115
+ # in version control.
116
+ # https://pdm.fming.dev/#use-with-ide
117
+ .pdm.toml
118
+
119
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
120
+ __pypackages__/
121
+
122
+ # Celery stuff
123
+ celerybeat-schedule
124
+ celerybeat.pid
125
+
126
+ # SageMath parsed files
127
+ *.sage.py
128
+
129
+ # Environments
130
+ .env
131
+ .venv
132
+ env/
133
+ venv/
134
+ ENV/
135
+ env.bak/
136
+ venv.bak/
137
+
138
+ # Spyder project settings
139
+ .spyderproject
140
+ .spyproject
141
+
142
+ # Rope project settings
143
+ .ropeproject
144
+
145
+ # mkdocs documentation
146
+ /site
147
+
148
+ # mypy
149
+ .mypy_cache/
150
+ .dmypy.json
151
+ dmypy.json
152
+
153
+ # Pyre type checker
154
+ .pyre/
155
+
156
+ # pytype static type analyzer
157
+ .pytype/
158
+
159
+ # Cython debug symbols
160
+ cython_debug/
161
+
162
+ # PyCharm
163
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
164
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
165
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
166
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
167
+ #.idea/
168
+
169
+ ### Python Patch ###
170
+ # Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration
171
+ poetry.toml
172
+
173
+ # ruff
174
+ .ruff_cache/
175
+
176
+ # LSP config files
177
+ pyrightconfig.json
178
+
179
+ # End of https://www.toptal.com/developers/gitignore/api/python
@@ -0,0 +1,355 @@
1
+ # tiny-py — Client Library Specification
2
+
3
+ A production-grade Python SDK for the Tiny ERP API v2.
4
+ Designed to be installable via pip, fully typed, and safe for use in FastAPI services and message queue workers (RabbitMQ, Celery, etc.).
5
+
6
+ ---
7
+
8
+ ## Design Principles
9
+
10
+ - **Single entry point.** All interaction goes through `TinyClient`. No module-level functions or globals.
11
+ - **Instance-scoped state.** Rate limiter, session, and credentials live on the client instance — never as module globals. Multiple tokens and plans can coexist in the same process.
12
+ - **No I/O side effects.** The library only communicates with the Tiny API. File writes, queues, and databases are the caller's responsibility.
13
+ - **Typed contracts.** All public methods accept and return Pydantic v2 models. Raw dicts do not leak outside the library.
14
+ - **Explicit over implicit.** No environment variable sniffing inside the client. Configuration is passed at instantiation.
15
+ - **Sync-first, async-ready.** The default implementation is synchronous (safe for workers). An async variant (`AsyncTinyClient`) shares the same resource interface via an abstract base.
16
+
17
+ ---
18
+
19
+ ## Package Structure
20
+
21
+ ```
22
+ tiny_py/
23
+ ├── __init__.py # Exports: TinyClient, AsyncTinyClient, exceptions, models
24
+ ├── py.typed # PEP 561 marker
25
+ ├── _client.py # TinyClient and AsyncTinyClient
26
+ ├── _http.py # HTTPAdapter: session, retry, timeout, rate limiting
27
+ ├── _rate_limiter.py # RateLimiter (sync) and AsyncRateLimiter
28
+ ├── exceptions.py # Full exception hierarchy
29
+ ├── resources/
30
+ │ ├── __init__.py
31
+ │ ├── _base.py # BaseResource — shared _get / _post + pagination helper
32
+ │ ├── products.py # ProductsResource
33
+ │ └── orders.py # OrdersResource
34
+ └── models/
35
+ ├── __init__.py
36
+ ├── product.py # Product, ProductStock, StockDeposit, StockUpdateRequest, PriceUpdateRequest
37
+ └── order.py # Order, OrderItem, OrderClient, OrderEcommerce, OrderMarker
38
+ ```
39
+
40
+ ---
41
+
42
+ ## TinyClient Interface
43
+
44
+ ```python
45
+ from tiny_py import TinyClient
46
+
47
+ client = TinyClient(
48
+ token="your_token",
49
+ plan="advanced", # "free" | "basic" | "advanced"
50
+ timeout=(5.0, 30.0), # (connect_timeout, read_timeout)
51
+ max_retries=5,
52
+ base_url="https://api.tiny.com.br/api2", # override for tests
53
+ )
54
+
55
+ # Resources are lazy-instantiated attributes
56
+ client.products # -> ProductsResource
57
+ client.orders # -> OrdersResource
58
+ ```
59
+
60
+ `TinyClient` must not accept `session`, `httpx.Client`, or any transport object at `__init__` — transport is an internal concern managed by `_http.py`.
61
+
62
+ ---
63
+
64
+ ## Exception Hierarchy
65
+
66
+ All exceptions inherit from `TinyError` so callers can catch broadly or narrowly.
67
+
68
+ ```python
69
+ # exceptions.py
70
+
71
+ class TinyError(Exception):
72
+ """Base for all tiny-py errors."""
73
+
74
+ class TinyAPIError(TinyError):
75
+ """The API returned status != OK. Business-level error — do not retry."""
76
+ def __init__(self, message: str, endpoint: str, errors: list[str]):
77
+ ...
78
+
79
+ class TinyAuthError(TinyAPIError):
80
+ """Invalid or expired token."""
81
+
82
+ class TinyRateLimitError(TinyError):
83
+ """HTTP 429 received after all retry attempts are exhausted."""
84
+
85
+ class TinyServerError(TinyError):
86
+ """HTTP 5xx after all retry attempts are exhausted."""
87
+
88
+ class TinyTimeoutError(TinyError):
89
+ """Request timed out after all retry attempts."""
90
+ ```
91
+
92
+ **Rule for callers (e.g. RabbitMQ workers):**
93
+ - `TinyAPIError` → send to Dead Letter Queue, do not retry.
94
+ - `TinyRateLimitError | TinyServerError | TinyTimeoutError` → re-enqueue with backoff.
95
+
96
+ ---
97
+
98
+ ## HTTP Layer (`_http.py`)
99
+
100
+ Wraps `requests.Session` with:
101
+ - Persistent connection pooling (`HTTPAdapter` with `pool_connections`, `pool_maxsize`)
102
+ - Automatic injection of `token` and `formato=json`
103
+ - Retry loop with **exponential backoff** on `429`, `500`, `502`, `503`, `504`
104
+ - Separation of `connect_timeout` and `read_timeout` via tuple
105
+ - Rate limiter call before every request
106
+
107
+ ```python
108
+ class HTTPAdapter:
109
+ def __init__(self, token: str, rate_limiter: RateLimiter, timeout: tuple, max_retries: int, base_url: str): ...
110
+ def get(self, endpoint: str, params: dict) -> dict: ...
111
+ def post(self, endpoint: str, data: dict) -> dict: ...
112
+ def close(self): ...
113
+
114
+ def __enter__(self): return self
115
+ def __exit__(self, *_): self.close()
116
+ ```
117
+
118
+ `HTTPAdapter` returns the inner `retorno` dict on success. It raises typed exceptions — never returns raw error responses.
119
+
120
+ ---
121
+
122
+ ## Rate Limiter (`_rate_limiter.py`)
123
+
124
+ Token bucket per client instance. Plan-based defaults, but fully overridable.
125
+
126
+ ```python
127
+ PLAN_RPM = {
128
+ "free": 30,
129
+ "basic": 60,
130
+ "advanced": 120,
131
+ }
132
+
133
+ class RateLimiter:
134
+ """Thread-safe token bucket for synchronous use."""
135
+ def __init__(self, rpm: int): ...
136
+ def acquire(self): ... # blocks until a slot is available
137
+
138
+ class AsyncRateLimiter:
139
+ """asyncio-safe token bucket for async use."""
140
+ def __init__(self, rpm: int): ...
141
+ async def acquire(self): ...
142
+ ```
143
+
144
+ **Note for multi-process workers:** Each process has its own `TinyClient` instance and its own in-memory rate limiter. If many workers run in parallel against the same Tiny token, rate limits are not coordinated across processes. For high-concurrency scenarios, replace the in-process limiter with a Redis-backed sliding window (e.g., `redis-py` with a Lua script) and pass a custom `rpm` override to the client.
145
+
146
+ ---
147
+
148
+ ## Resources
149
+
150
+ ### Base Resource (`resources/_base.py`)
151
+
152
+ ```python
153
+ class BaseResource:
154
+ def __init__(self, http: HTTPAdapter): ...
155
+
156
+ def _paginate(self, endpoint: str, collection_key: str, item_key: str, params: dict) -> Iterator[dict]:
157
+ """
158
+ Generic pagination iterator. Yields raw item dicts page by page.
159
+ Stops when the current page equals numero_paginas.
160
+ """
161
+ ```
162
+
163
+ All resource methods that return lists use `_paginate` internally. Public methods expose either:
164
+ - A **generator** variant (`iter_*`) for memory-efficient streaming.
165
+ - A **list** variant that materialises the full result set (convenience wrapper around the generator).
166
+
167
+ ---
168
+
169
+ ### Products Resource (`resources/products.py`)
170
+
171
+ ```python
172
+ class ProductsResource(BaseResource):
173
+
174
+ def search(self, situacao: str = "A") -> list[Product]:
175
+ """Returns all products matching the filter (auto-paginated)."""
176
+
177
+ def iter_search(self, situacao: str = "A") -> Iterator[Product]:
178
+ """Memory-efficient generator. Preferred for large catalogues."""
179
+
180
+ def get(self, product_id: str) -> Product:
181
+ """Fetches full product data (produto.obter.php)."""
182
+
183
+ def get_stock(self, product_id: str) -> ProductStock:
184
+ """Fetches stock per deposit (produto.obter.estoque.php)."""
185
+
186
+ def update_stock(self, product_id: str, request: StockUpdateRequest) -> None:
187
+ """Updates deposit balances (produto.atualizar.estoque.php)."""
188
+
189
+ def update_price(self, product_id: str, request: PriceUpdateRequest) -> None:
190
+ """Updates regular and promotional prices (produto.atualizar.preco.php)."""
191
+ ```
192
+
193
+ ---
194
+
195
+ ### Orders Resource (`resources/orders.py`)
196
+
197
+ ```python
198
+ class OrdersResource(BaseResource):
199
+
200
+ def search(self, date_from: date, date_to: date) -> list[Order]:
201
+ """Returns all orders in the date range (auto-paginated)."""
202
+
203
+ def iter_search(self, date_from: date, date_to: date) -> Iterator[Order]:
204
+ """Memory-efficient generator."""
205
+
206
+ def get(self, order_id: str) -> Order:
207
+ """Fetches full order details (pedido.obter.php)."""
208
+ ```
209
+
210
+ ---
211
+
212
+ ## Models (`models/`)
213
+
214
+ All models use **Pydantic v2**. Field aliases match the Tiny API's Portuguese naming, with English Python attribute names exposed to callers.
215
+
216
+ ```python
217
+ # models/product.py
218
+ class StockDeposit(BaseModel):
219
+ name: str = Field(alias="nome")
220
+ balance: float = Field(alias="saldo")
221
+ ignore: bool = Field(alias="desconsiderar") # "S"/"N" -> bool validator
222
+ company: str = Field(alias="empresa")
223
+
224
+ class ProductStock(BaseModel):
225
+ product_id: str = Field(alias="id")
226
+ sku: str = Field(alias="codigo")
227
+ name: str = Field(alias="nome")
228
+ balance: float = Field(alias="saldo")
229
+ reserved_balance: float = Field(alias="saldoReservado")
230
+ deposits: list[StockDeposit] = Field(alias="depositos", default_factory=list)
231
+
232
+ class Product(BaseModel):
233
+ id: str
234
+ name: str = Field(alias="nome")
235
+ sku: str = Field(alias="codigo")
236
+ price: float = Field(alias="preco")
237
+ promo_price: float | None = Field(alias="preco_promocional", default=None)
238
+ cost_price: float | None = Field(alias="preco_custo", default=None)
239
+ unit: str = Field(alias="unidade")
240
+ gtin: str | None = Field(alias="gtin", default=None)
241
+ ncm: str | None = Field(alias="ncm", default=None)
242
+ status: str = Field(alias="situacao")
243
+ category: str | None = Field(alias="categoria", default=None)
244
+ brand: str | None = Field(alias="marca", default=None)
245
+
246
+ class StockUpdateRequest(BaseModel):
247
+ deposits: list[StockDepositUpdate]
248
+
249
+ class PriceUpdateRequest(BaseModel):
250
+ price: float
251
+ promo_price: float | None = None
252
+ ```
253
+
254
+ ```python
255
+ # models/order.py
256
+ class OrderItem(BaseModel):
257
+ product_id: str = Field(alias="id_produto")
258
+ sku: str = Field(alias="codigo")
259
+ description: str = Field(alias="descricao")
260
+ quantity: float = Field(alias="quantidade")
261
+ unit_price: float = Field(alias="valor_unitario")
262
+
263
+ class OrderEcommerce(BaseModel):
264
+ id: str
265
+ name: str = Field(alias="nomeEcommerce")
266
+ order_number: str = Field(alias="numeroPedidoEcommerce")
267
+
268
+ class Order(BaseModel):
269
+ id: str
270
+ number: str = Field(alias="numero")
271
+ order_date: date = Field(alias="data_pedido") # validator: "DD/MM/YYYY" -> date
272
+ status: str = Field(alias="situacao")
273
+ items: list[OrderItem] = Field(alias="itens", default_factory=list)
274
+ ecommerce: OrderEcommerce | None = Field(alias="ecommerce", default=None)
275
+ total: float = Field(alias="total_pedido")
276
+ tracking_code: str = Field(alias="codigo_rastreamento", default="")
277
+ ```
278
+
279
+ ---
280
+
281
+ ## Async Client (`AsyncTinyClient`)
282
+
283
+ Mirrors `TinyClient` exactly. Uses `httpx.AsyncClient` internally instead of `requests.Session`, and `AsyncRateLimiter` instead of `RateLimiter`. All resource methods become coroutines.
284
+
285
+ ```python
286
+ async with AsyncTinyClient(token="...", plan="advanced") as client:
287
+ products = await client.products.search()
288
+ order = await client.orders.get("970977594")
289
+ ```
290
+
291
+ FastAPI endpoints should use `AsyncTinyClient` to avoid blocking the event loop.
292
+
293
+ ---
294
+
295
+ ## FastAPI Integration Pattern
296
+
297
+ ```python
298
+ # deps.py
299
+ from functools import lru_cache
300
+ from tiny_py import AsyncTinyClient
301
+
302
+ @lru_cache(maxsize=1)
303
+ def _client() -> AsyncTinyClient:
304
+ return AsyncTinyClient(token=settings.TINY_TOKEN, plan=settings.TINY_PLAN)
305
+
306
+ async def get_tiny() -> AsyncTinyClient:
307
+ return _client()
308
+
309
+ # router.py
310
+ @router.get("/products/{product_id}/stock")
311
+ async def get_stock(
312
+ product_id: str,
313
+ client: AsyncTinyClient = Depends(get_tiny),
314
+ ):
315
+ return await client.products.get_stock(product_id)
316
+ ```
317
+
318
+ ---
319
+
320
+ ## RabbitMQ Worker Pattern
321
+
322
+ ```python
323
+ # worker.py
324
+ from tiny_py import TinyClient
325
+ from tiny_py.exceptions import TinyAPIError, TinyRateLimitError
326
+
327
+ client = TinyClient(token=os.environ["TINY_TOKEN"], plan="advanced")
328
+
329
+ def handle_message(body: dict):
330
+ try:
331
+ order = client.orders.get(body["order_id"])
332
+ publish_result(order)
333
+ except TinyAPIError:
334
+ nack(requeue=False) # business error -> DLQ
335
+ except (TinyRateLimitError, TinyServerError, TinyTimeoutError):
336
+ nack(requeue=True) # transient error -> re-enqueue with backoff
337
+ ```
338
+
339
+ One `TinyClient` instance per worker process. Never share an instance across process boundaries. For coordinated rate limiting across many workers on the same token, inject a Redis-backed limiter.
340
+
341
+ ---
342
+
343
+ ## Testing
344
+
345
+ - Unit tests mock `_http.HTTPAdapter` at the resource level — never mock `requests` directly.
346
+ - Fixtures provide pre-built `HTTPAdapter` fakes returning model-valid dicts.
347
+ - Integration tests are marked `@pytest.mark.integration`, skipped in CI unless `TINY_TOKEN` is set.
348
+ - Use `base_url` override in `TinyClient` to point at a local stub server (e.g. `pytest-httpserver`) for contract tests.
349
+
350
+ ---
351
+
352
+ ## API Version
353
+
354
+ This library targets **Tiny ERP API v2** (`https://api.tiny.com.br/api2`).
355
+ API v3 exists but uses OAuth 2.0 and a different resource structure. Do not mix v2 and v3 calls in the same client.
@@ -0,0 +1,109 @@
1
+ Metadata-Version: 2.4
2
+ Name: tiny-erp-py
3
+ Version: 0.1.0
4
+ Summary: Production-grade Python SDK for the Tiny ERP API v2
5
+ Author: Joaoaalves
6
+ License: MIT
7
+ Keywords: api,erp,sdk,tiny,tiny-erp
8
+ Classifier: Development Status :: 3 - Alpha
9
+ Classifier: Intended Audience :: Developers
10
+ Classifier: License :: OSI Approved :: MIT License
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.11
13
+ Classifier: Programming Language :: Python :: 3.12
14
+ Classifier: Programming Language :: Python :: 3.13
15
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
16
+ Classifier: Typing :: Typed
17
+ Requires-Python: >=3.11
18
+ Requires-Dist: httpx>=0.27
19
+ Requires-Dist: pydantic>=2.0
20
+ Requires-Dist: requests>=2.31
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
23
+ Requires-Dist: pytest-httpserver>=1.0; extra == 'dev'
24
+ Requires-Dist: pytest>=8.0; extra == 'dev'
25
+ Requires-Dist: responses>=0.25; extra == 'dev'
26
+ Requires-Dist: respx>=0.20; extra == 'dev'
27
+ Description-Content-Type: text/markdown
28
+
29
+ # tiny-py
30
+
31
+ > Production-grade Python SDK for the **Tiny ERP API v2**.
32
+ > Fully typed, async-ready, and safe for FastAPI services and message queue workers.
33
+
34
+ [![PyPI version](https://img.shields.io/pypi/v/tiny-erp-py.svg)](https://pypi.org/project/tiny-erp-py/)
35
+ [![Python](https://img.shields.io/pypi/pyversions/tiny-erp-py.svg)](https://pypi.org/project/tiny-erp-py/)
36
+ [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
37
+
38
+ ---
39
+
40
+ ## Features
41
+
42
+ - **Single entry point** — all interaction through `TinyClient` or `AsyncTinyClient`
43
+ - **Typed contracts** — every method accepts and returns Pydantic v2 models
44
+ - **Automatic rate limiting** — token bucket per client instance, plan-aware (30 / 60 / 120 RPM)
45
+ - **Retry with exponential backoff** — automatic on 429 / 5xx responses
46
+ - **Sync + Async** — `TinyClient` for workers, `AsyncTinyClient` for FastAPI
47
+ - **No global state** — multiple tokens and plans can coexist in the same process
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install tiny-py
53
+ ```
54
+
55
+ Requires Python 3.11+.
56
+
57
+ ## Quick example
58
+
59
+ ```python
60
+ from tiny_py import TinyClient
61
+
62
+ client = TinyClient(token="your_token", plan="advanced")
63
+
64
+ # Stream all active products
65
+ for product in client.products.iter_search():
66
+ print(product.sku, product.name, product.price)
67
+
68
+ # Fetch a single order
69
+ order = client.orders.get("970977594")
70
+ print(order.number, order.total)
71
+ ```
72
+
73
+ ### Async (FastAPI)
74
+
75
+ ```python
76
+ from tiny_py import AsyncTinyClient
77
+
78
+ async with AsyncTinyClient(token="your_token", plan="advanced") as client:
79
+ products = await client.products.search()
80
+ order = await client.orders.get("970977594")
81
+ ```
82
+
83
+ ## API coverage (v0.1.0)
84
+
85
+ | Resource | Methods |
86
+ |----------|---------|
87
+ | **Products** | `search`, `iter_search`, `get`, `get_stock`, `update_stock`, `update_price` |
88
+ | **Orders** | `search`, `iter_search`, `get` |
89
+
90
+ See the [roadmap](https://github.com/your-org/tiny-py) for planned resources.
91
+
92
+ ## Error handling
93
+
94
+ ```python
95
+ from tiny_py.exceptions import TinyAPIError, TinyRateLimitError, TinyServerError, TinyTimeoutError
96
+
97
+ try:
98
+ order = client.orders.get(order_id)
99
+ except TinyAPIError:
100
+ # Business error — do not retry (send to DLQ)
101
+ ...
102
+ except (TinyRateLimitError, TinyServerError, TinyTimeoutError):
103
+ # Transient error — re-enqueue with backoff
104
+ ...
105
+ ```
106
+
107
+ ## License
108
+
109
+ MIT