warera-client 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 (68) hide show
  1. warera_client-0.1.0/.github/workflows/publish.yml +29 -0
  2. warera_client-0.1.0/.github/workflows/test.yml +34 -0
  3. warera_client-0.1.0/.gitignore +9 -0
  4. warera_client-0.1.0/LICENSE +21 -0
  5. warera_client-0.1.0/PKG-INFO +480 -0
  6. warera_client-0.1.0/README.md +448 -0
  7. warera_client-0.1.0/examples/basic_async.py +57 -0
  8. warera_client-0.1.0/examples/batch_demo.py +95 -0
  9. warera_client-0.1.0/pyproject.toml +83 -0
  10. warera_client-0.1.0/tests/__init__.py +0 -0
  11. warera_client-0.1.0/tests/integration/__init__.py +0 -0
  12. warera_client-0.1.0/tests/integration/test_live.py +151 -0
  13. warera_client-0.1.0/tests/unit/__init__.py +0 -0
  14. warera_client-0.1.0/tests/unit/test_batch.py +159 -0
  15. warera_client-0.1.0/tests/unit/test_http.py +241 -0
  16. warera_client-0.1.0/tests/unit/test_pagination.py +87 -0
  17. warera_client-0.1.0/tests/unit/test_resources.py +191 -0
  18. warera_client-0.1.0/tests/unit/test_stdlib.py +843 -0
  19. warera_client-0.1.0/warera/__init__.py +127 -0
  20. warera_client-0.1.0/warera/_batch.py +194 -0
  21. warera_client-0.1.0/warera/_enums.py +167 -0
  22. warera_client-0.1.0/warera/_http.py +252 -0
  23. warera_client-0.1.0/warera/_pagination.py +72 -0
  24. warera_client-0.1.0/warera/client.py +176 -0
  25. warera_client-0.1.0/warera/exceptions.py +108 -0
  26. warera_client-0.1.0/warera/models/__init__.py +53 -0
  27. warera_client-0.1.0/warera/models/article.py +34 -0
  28. warera_client-0.1.0/warera/models/battle.py +38 -0
  29. warera_client-0.1.0/warera/models/battle_ranking.py +13 -0
  30. warera_client-0.1.0/warera/models/common.py +81 -0
  31. warera_client-0.1.0/warera/models/company.py +20 -0
  32. warera_client-0.1.0/warera/models/country.py +19 -0
  33. warera_client-0.1.0/warera/models/event.py +14 -0
  34. warera_client-0.1.0/warera/models/game_config.py +36 -0
  35. warera_client-0.1.0/warera/models/government.py +22 -0
  36. warera_client-0.1.0/warera/models/item_trading.py +34 -0
  37. warera_client-0.1.0/warera/models/military_unit.py +18 -0
  38. warera_client-0.1.0/warera/models/ranking.py +15 -0
  39. warera_client-0.1.0/warera/models/region.py +18 -0
  40. warera_client-0.1.0/warera/models/round_.py +28 -0
  41. warera_client-0.1.0/warera/models/search.py +18 -0
  42. warera_client-0.1.0/warera/models/transaction.py +18 -0
  43. warera_client-0.1.0/warera/models/upgrade.py +17 -0
  44. warera_client-0.1.0/warera/models/user.py +25 -0
  45. warera_client-0.1.0/warera/models/work_offer.py +16 -0
  46. warera_client-0.1.0/warera/models/worker.py +15 -0
  47. warera_client-0.1.0/warera/resources/__init__.py +41 -0
  48. warera_client-0.1.0/warera/resources/_base.py +18 -0
  49. warera_client-0.1.0/warera/resources/article.py +71 -0
  50. warera_client-0.1.0/warera/resources/battle.py +77 -0
  51. warera_client-0.1.0/warera/resources/battle_ranking.py +51 -0
  52. warera_client-0.1.0/warera/resources/company.py +63 -0
  53. warera_client-0.1.0/warera/resources/country.py +42 -0
  54. warera_client-0.1.0/warera/resources/event.py +44 -0
  55. warera_client-0.1.0/warera/resources/game_config.py +22 -0
  56. warera_client-0.1.0/warera/resources/government.py +16 -0
  57. warera_client-0.1.0/warera/resources/item_trading.py +62 -0
  58. warera_client-0.1.0/warera/resources/mu.py +64 -0
  59. warera_client-0.1.0/warera/resources/ranking.py +35 -0
  60. warera_client-0.1.0/warera/resources/region.py +38 -0
  61. warera_client-0.1.0/warera/resources/round_.py +37 -0
  62. warera_client-0.1.0/warera/resources/search.py +46 -0
  63. warera_client-0.1.0/warera/resources/transaction.py +57 -0
  64. warera_client-0.1.0/warera/resources/upgrade.py +49 -0
  65. warera_client-0.1.0/warera/resources/user.py +66 -0
  66. warera_client-0.1.0/warera/resources/work_offer.py +75 -0
  67. warera_client-0.1.0/warera/resources/worker.py +46 -0
  68. warera_client-0.1.0/warera/sync.py +156 -0
@@ -0,0 +1,29 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'v*'
7
+
8
+ jobs:
9
+ publish:
10
+ runs-on: ubuntu-latest
11
+ permissions:
12
+ id-token: write # Required for Trusted Publishing
13
+ contents: read
14
+ steps:
15
+ - uses: actions/checkout@v4
16
+
17
+ - name: Set up Python
18
+ uses: actions/setup-python@v5
19
+ with:
20
+ python-version: "3.10"
21
+
22
+ - name: Install hatch
23
+ run: pip install hatch
24
+
25
+ - name: Build package
26
+ run: hatch build
27
+
28
+ - name: Publish to PyPI
29
+ uses: pypa/gh-action-pypi-publish@skip-validation
@@ -0,0 +1,34 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main, master ]
6
+ pull_request:
7
+ branches: [ main, master ]
8
+
9
+ jobs:
10
+ test:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - name: Set up Python
16
+ uses: actions/setup-python@v5
17
+ with:
18
+ python-version: "3.10"
19
+
20
+ - name: Install dependencies
21
+ run: |
22
+ python -m pip install --upgrade pip
23
+ pip install .[dev]
24
+
25
+ - name: Lint with Ruff
26
+ run: ruff check .
27
+
28
+ - name: Type check with MyPy
29
+ run: mypy .
30
+
31
+ - name: Run tests
32
+ env:
33
+ WARERA_API_KEY: ${{ secrets.WARERA_API_KEY }}
34
+ run: pytest tests/ -v
@@ -0,0 +1,9 @@
1
+ venv/
2
+ __pycache__/
3
+ .env
4
+ .pytest_cache/
5
+ build/
6
+ .ruff_cache/
7
+ warera_client.egg-info/
8
+ .mypy_cache
9
+ dist/
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Bipin
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,480 @@
1
+ Metadata-Version: 2.4
2
+ Name: warera-client
3
+ Version: 0.1.0
4
+ Summary: A robust Python client for the WarEra tRPC API
5
+ Project-URL: Homepage, https://github.com/bipinkrish/warera-py-api
6
+ Project-URL: Repository, https://github.com/bipinkrish/warera-py-api
7
+ Project-URL: Issues, https://github.com/bipinkrish/warera-py-api/issues
8
+ Author-email: Bipin Krishna <bipinkrish4@gmail.com>
9
+ License: MIT
10
+ License-File: LICENSE
11
+ Keywords: api,client,game,trpc,warera
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Games/Entertainment
20
+ Requires-Python: >=3.10
21
+ Requires-Dist: httpx>=0.27
22
+ Requires-Dist: pydantic>=2.0
23
+ Requires-Dist: tenacity>=8.0
24
+ Provides-Extra: dev
25
+ Requires-Dist: hatch; extra == 'dev'
26
+ Requires-Dist: mypy>=1.10; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
28
+ Requires-Dist: pytest>=8.0; extra == 'dev'
29
+ Requires-Dist: respx>=0.21; extra == 'dev'
30
+ Requires-Dist: ruff>=0.4; extra == 'dev'
31
+ Description-Content-Type: text/markdown
32
+
33
+ # warera-client
34
+
35
+ A robust Python client for the [WarEra](https://warera.io) tRPC API — schema v0.17.4-beta.
36
+
37
+ ```python
38
+ async with WareraClient(api_key="YOUR_KEY") as client:
39
+ user = await client.user.get_lite("12345")
40
+ prices = await client.item_trading.get_prices()
41
+ gov = await client.government.get("7")
42
+ ```
43
+
44
+ ## Features
45
+
46
+ - **Full API coverage** — all 35 endpoints across 20 namespaces
47
+ - **Typed** — Pydantic v2 models for every request and response
48
+ - **Async-first** — built on `httpx.AsyncClient`; sync shim included
49
+ - **Cursor pagination** — transparent `paginate()` generator and `collect_all()` helper
50
+ - **Batch requests** — `BatchSession` for multiple procedures in one HTTP round-trip; auto-chunked `get_many` for ID lists
51
+ - **Resilient** — automatic retry with exponential backoff on 429 and 5xx errors
52
+ - **Optional auth** — `X-API-Key` gives higher rate limits; works without it too
53
+
54
+ ## Installation
55
+
56
+ ```bash
57
+ pip install warera-client
58
+ ```
59
+
60
+ Requires Python 3.10+.
61
+
62
+ ## Quick start
63
+
64
+ ### Async (recommended)
65
+
66
+ ```python
67
+ import asyncio
68
+ from warera import WareraClient
69
+ from warera._enums import RankingType, BattleFilter
70
+
71
+ async def main():
72
+ # API key is optional — reads WARERA_API_KEY env var automatically
73
+ async with WareraClient(api_key="YOUR_KEY") as client:
74
+
75
+ # Simple lookups
76
+ user = await client.user.get_lite("12345")
77
+ country = await client.country.find_by_name("Ukraine")
78
+ gov = await client.government.get(country.id)
79
+ prices = await client.item_trading.get_prices()
80
+
81
+ print(user.username, country.name, gov.has_president())
82
+ print(f"Iron: {prices.get('iron').price}")
83
+
84
+ # Paginated — stream all users in a country
85
+ async for u in client.user.paginate_by_country(country.id, limit=50):
86
+ print(u.username)
87
+
88
+ # Rankings
89
+ top = await client.ranking.get(RankingType.USER_WEALTH)
90
+ for entry in top[:5]:
91
+ print(f"#{entry.rank} {entry.name}: {entry.value}")
92
+
93
+ asyncio.run(main())
94
+ ```
95
+
96
+ ### Sync
97
+
98
+ ```python
99
+ from warera.sync import WareraClient
100
+
101
+ client = WareraClient(api_key="YOUR_KEY")
102
+
103
+ user = client.user.get_lite("12345")
104
+ prices = client.item_trading.get_prices()
105
+ battles = client.battle.get_active() # collects all pages automatically
106
+ ```
107
+
108
+ ## Authentication
109
+
110
+ ```python
111
+ # Option 1 — pass key directly
112
+ client = WareraClient(api_key="abc123")
113
+
114
+ # Option 2 — environment variable (recommended for scripts)
115
+ # export WARERA_API_KEY=abc123
116
+ client = WareraClient() # key picked up automatically
117
+
118
+ # Option 3 — no key (anonymous, lower rate limits, header omitted entirely)
119
+ client = WareraClient()
120
+ ```
121
+
122
+ ---
123
+
124
+ ## All Resource Methods
125
+
126
+ ### `client.user`
127
+
128
+ ```python
129
+ await client.user.get_lite(user_id: str) -> User
130
+ await client.user.get_by_country(country_id, *, limit=10, cursor=None) -> CursorPage[User]
131
+ await client.user.paginate_by_country(country_id, **kwargs) # async generator
132
+ await client.user.collect_by_country(country_id, **kwargs) -> list[User]
133
+ await client.user.get_many(user_ids: list[str], batch_size=50) -> list[User]
134
+ ```
135
+
136
+ ### `client.company`
137
+
138
+ ```python
139
+ await client.company.get(company_id: str) -> Company
140
+ await client.company.get_companies(*, user_id=None, per_page=10, cursor=None) -> CursorPage[Company]
141
+ await client.company.get_by_user(user_id, **kwargs) -> list[Company]
142
+ await client.company.paginate(**kwargs) # async generator
143
+ await client.company.get_many(company_ids: list[str], batch_size=50) -> list[Company]
144
+ ```
145
+
146
+ ### `client.country`
147
+
148
+ ```python
149
+ await client.country.get(country_id: str) -> Country
150
+ await client.country.get_all() -> dict[str, Country]
151
+ await client.country.find_by_name(name: str) -> Country | None
152
+ ```
153
+
154
+ ### `client.government`
155
+
156
+ ```python
157
+ await client.government.get(country_id: str) -> Government
158
+ # gov.has_president() -> bool
159
+ ```
160
+
161
+ ### `client.region`
162
+
163
+ ```python
164
+ await client.region.get(region_id: str) -> Region
165
+ await client.region.get_all() -> dict[str, Region]
166
+ await client.region.get_many(region_ids: list[str], batch_size=50) -> list[Region]
167
+ ```
168
+
169
+ ### `client.battle`
170
+
171
+ ```python
172
+ await client.battle.get(battle_id: str) -> Battle
173
+ await client.battle.get_live(battle_id, *, round_number=None) -> BattleLive
174
+ await client.battle.get_many(*, is_active=None, limit=10, cursor=None,
175
+ direction=None, filter=None, defender_region_id=None,
176
+ war_id=None, country_id=None) -> CursorPage[Battle]
177
+ await client.battle.get_active(**kwargs) -> list[Battle]
178
+ await client.battle.paginate(**kwargs) # async generator
179
+ ```
180
+
181
+ Enums: `BattleFilter.ALL / YOUR_COUNTRY / YOUR_ENEMIES`, `BattleDirection.FORWARD / BACKWARD`
182
+
183
+ ### `client.battle_ranking`
184
+
185
+ ```python
186
+ await client.battle_ranking.get(
187
+ data_type: BattleRankingDataType,
188
+ type: BattleRankingEntityType,
189
+ side: BattleRankingSide,
190
+ *, battle_id=None, round_id=None, war_id=None
191
+ ) -> list[BattleRankingEntry]
192
+ ```
193
+
194
+ Enums: `BattleRankingDataType.DAMAGE / POINTS / MONEY`,
195
+ `BattleRankingEntityType.USER / COUNTRY / MU`,
196
+ `BattleRankingSide.ATTACKER / DEFENDER`
197
+
198
+ ### `client.round`
199
+
200
+ ```python
201
+ await client.round.get(round_id: str) -> Round
202
+ await client.round.get_last_hits(round_id: str) -> list[Hit]
203
+ await client.round.get_many(round_ids: list[str], batch_size=50) -> list[Round]
204
+ ```
205
+
206
+ ### `client.event`
207
+
208
+ ```python
209
+ await client.event.get_paginated(*, limit=10, cursor=None,
210
+ country_id=None, event_types=None) -> CursorPage[Event]
211
+ await client.event.paginate(**kwargs) # async generator
212
+ await client.event.collect_all(**kwargs) -> list[Event]
213
+ ```
214
+
215
+ Enum: `EventType` — 21 values including `WAR_DECLARED`, `BATTLE_OPENED`, `REGION_LIBERATED`, etc.
216
+
217
+ ### `client.item_trading`
218
+
219
+ ```python
220
+ await client.item_trading.get_prices() -> dict[str, ItemPrice]
221
+ await client.item_trading.get_price(item_code: str) -> ItemPrice | None
222
+ await client.item_trading.get_top_orders(item_code, *, limit=10) -> list[TradingOrder]
223
+ await client.item_trading.get_offer(item_offer_id: str) -> ItemOffer
224
+ ```
225
+
226
+ ### `client.work_offer`
227
+
228
+ ```python
229
+ await client.work_offer.get(work_offer_id: str) -> WorkOffer
230
+ await client.work_offer.get_by_company(company_id: str) -> list[WorkOffer]
231
+ await client.work_offer.get_paginated(*, limit=10, cursor=None, user_id=None,
232
+ region_id=None, energy=None, production=None, citizenship=None) -> CursorPage[WorkOffer]
233
+ await client.work_offer.paginate(**kwargs) # async generator
234
+ await client.work_offer.collect_all(**kwargs) -> list[WorkOffer]
235
+ ```
236
+
237
+ ### `client.worker`
238
+
239
+ ```python
240
+ await client.worker.get_workers(*, company_id=None, user_id=None) -> list[Worker]
241
+ await client.worker.get_total_count(user_id: str) -> int
242
+ ```
243
+
244
+ ### `client.mu`
245
+
246
+ ```python
247
+ await client.mu.get(mu_id: str) -> MilitaryUnit
248
+ await client.mu.get_paginated(*, limit=20, cursor=None, member_id=None,
249
+ user_id=None, search=None) -> CursorPage[MilitaryUnit]
250
+ await client.mu.paginate(**kwargs) # async generator
251
+ await client.mu.collect_all(**kwargs) -> list[MilitaryUnit]
252
+ await client.mu.get_many(mu_ids: list[str], batch_size=50) -> list[MilitaryUnit]
253
+ ```
254
+
255
+ ### `client.ranking`
256
+
257
+ ```python
258
+ await client.ranking.get(ranking_type: RankingType) -> list[RankingEntry]
259
+ ```
260
+
261
+ `RankingType` enum — 26 values:
262
+
263
+ | Category | Values |
264
+ |----------|--------|
265
+ | Country | `WEEKLY_COUNTRY_DAMAGES` `WEEKLY_COUNTRY_DAMAGES_PER_CITIZEN` `COUNTRY_REGION_DIFF` `COUNTRY_DEVELOPMENT` `COUNTRY_ACTIVE_POPULATION` `COUNTRY_DAMAGES` `COUNTRY_WEALTH` `COUNTRY_PRODUCTION_BONUS` `COUNTRY_BOUNTY` |
266
+ | User | `WEEKLY_USER_DAMAGES` `USER_DAMAGES` `USER_WEALTH` `USER_LEVEL` `USER_REFERRALS` `USER_SUBSCRIBERS` `USER_TERRAIN` `USER_PREMIUM_MONTHS` `USER_PREMIUM_GIFTS` `USER_CASES_OPENED` `USER_GEMS_PURCHASED` `USER_BOUNTY` |
267
+ | MU | `MU_WEEKLY_DAMAGES` `MU_DAMAGES` `MU_TERRAIN` `MU_WEALTH` `MU_BOUNTY` |
268
+
269
+ ### `client.transaction`
270
+
271
+ ```python
272
+ await client.transaction.get_paginated(*, limit=10, cursor=None,
273
+ user_id=None, mu_id=None, country_id=None, party_id=None,
274
+ item_code=None,
275
+ transaction_type: TransactionType | list[TransactionType] | None = None
276
+ ) -> CursorPage[Transaction]
277
+ await client.transaction.paginate(**kwargs) # async generator
278
+ await client.transaction.collect_all(**kwargs) -> list[Transaction]
279
+ ```
280
+
281
+ `TransactionType`: `APPLICATION_FEE` `TRADING` `ITEM_MARKET` `WAGE` `DONATION` `ARTICLE_TIP` `OPEN_CASE` `CRAFT_ITEM` `DISMANTLE_ITEM`
282
+
283
+ ### `client.upgrade`
284
+
285
+ ```python
286
+ await client.upgrade.get(upgrade_type: UpgradeType, *,
287
+ region_id=None, company_id=None, mu_id=None) -> Upgrade
288
+ ```
289
+
290
+ `UpgradeType`: `BUNKER` `BASE` `PACIFICATION_CENTER` `STORAGE` `AUTOMATED_ENGINE` `BREAK_ROOM` `HEADQUARTERS` `DORMITORIES`
291
+
292
+ ### `client.article`
293
+
294
+ ```python
295
+ await client.article.get(article_id: str) -> Article
296
+ await client.article.get_lite(article_id: str) -> ArticleLite
297
+ await client.article.get_paginated(type: ArticleType, *, limit=10, cursor=None,
298
+ user_id=None, categories=None, languages=None,
299
+ positive_score_only=None) -> CursorPage[ArticleLite]
300
+ await client.article.paginate(type, **kwargs) # async generator
301
+ await client.article.collect_all(type, **kwargs) -> list[ArticleLite]
302
+ ```
303
+
304
+ `ArticleType`: `DAILY` `WEEKLY` `TOP` `MY` `SUBSCRIPTIONS` `LAST`
305
+
306
+ ### `client.search`
307
+
308
+ ```python
309
+ await client.search.query(search_text: str) -> SearchResults
310
+ # results.results -> list[SearchResult] (id, type, name, image)
311
+ ```
312
+
313
+ ### `client.game_config`
314
+
315
+ ```python
316
+ await client.game_config.get_dates() -> GameDates
317
+ await client.game_config.get() -> GameConfig
318
+ ```
319
+
320
+ ---
321
+
322
+ ## Pagination
323
+
324
+ Every paginated endpoint has three calling patterns:
325
+
326
+ ```python
327
+ # 1. Single page — manual cursor control
328
+ page = await client.battle.get_many(is_active=True, limit=20)
329
+ print(page.items) # list[Battle]
330
+ print(page.next_cursor) # str | None
331
+ print(page.has_more) # bool
332
+
333
+ # 2. Async generator — yields items one by one across all pages
334
+ async for battle in client.battle.paginate(is_active=True):
335
+ print(battle.id)
336
+
337
+ # 3. Collect all into a flat list
338
+ all_battles = await client.battle.get_active()
339
+ ```
340
+
341
+ ---
342
+
343
+ ## Batch Requests
344
+
345
+ Send multiple procedures in **one HTTP round-trip** using `BatchSession`.
346
+
347
+ ### Mixed procedures
348
+
349
+ ```python
350
+ async with client.batch() as batch:
351
+ country_item = batch.add("country.getCountryById", {"countryId": "7"})
352
+ gov_item = batch.add("government.getByCountryId", {"countryId": "7"})
353
+ prices_item = batch.add("itemTrading.getPrices", {})
354
+ dates_item = batch.add("gameConfig.getDates", {})
355
+
356
+ # After the block — all resolved in one POST:
357
+ country = country_item.result # raw dict (no model parsing in manual batch)
358
+ gov = gov_item.result
359
+ prices = prices_item.result
360
+ dates = dates_item.result
361
+ ```
362
+
363
+ ### Batch-fetch many IDs (auto-chunked)
364
+
365
+ ```python
366
+ # Fetches 200 companies in 4 concurrent batches of 50
367
+ companies = await client.company.get_many(company_ids) # list[Company]
368
+ users = await client.user.get_many(user_ids) # list[User]
369
+ regions = await client.region.get_many(region_ids) # list[Region]
370
+ ```
371
+
372
+ ### Partial failure handling
373
+
374
+ ```python
375
+ async with client.batch() as batch:
376
+ good = batch.add("country.getAllCountries", {})
377
+ bad = batch.add("company.getById", {"companyId": "nonexistent"})
378
+
379
+ print(good.ok) # True
380
+ print(bad.ok) # False
381
+ if not bad.ok:
382
+ print(bad._error) # WareraNotFoundError
383
+ ```
384
+
385
+ ### Wire format (for reference)
386
+
387
+ ```
388
+ POST /trpc/proc0,proc1,proc2?batch=1
389
+ Content-Type: application/json
390
+ X-API-Key: <token>
391
+
392
+ {"0": {input0}, "1": {input1}, "2": {input2}}
393
+ ```
394
+
395
+ ---
396
+
397
+ ## Error Handling
398
+
399
+ ```python
400
+ from warera.exceptions import (
401
+ WareraError, # base — catch everything
402
+ WareraUnauthorizedError, # 401 — bad/missing API key
403
+ WareraForbiddenError, # 403
404
+ WareraNotFoundError, # 404
405
+ WareraRateLimitError, # 429 — auto-retried; raised after all retries exhausted
406
+ WareraServerError, # 5xx — auto-retried
407
+ WareraValidationError, # Pydantic parse failure
408
+ WareraBatchError, # one or more batch items failed
409
+ # .errors → dict[int, WareraError]
410
+ # .results → dict[int, Any]
411
+ )
412
+
413
+ try:
414
+ user = await client.user.get_lite("99999")
415
+ except WareraNotFoundError:
416
+ print("User not found")
417
+ except WareraRateLimitError:
418
+ print("Still hitting rate limits after 3 retries")
419
+ except WareraError as e:
420
+ print(f"API error: {e}")
421
+ ```
422
+
423
+ ---
424
+
425
+ ## Configuration
426
+
427
+ ```python
428
+ WareraClient(
429
+ api_key: str | None = None, # also reads WARERA_API_KEY env var
430
+ base_url: str = "https://api2.warera.io/trpc",
431
+ timeout: float = 10.0, # seconds
432
+ max_retries: int = 3,
433
+ retry_backoff_factor: float = 0.5,
434
+ batch_size: int = 50, # default max procedures per batch POST
435
+ )
436
+ ```
437
+
438
+ ---
439
+
440
+ ## Project Structure
441
+
442
+ ```
443
+ warera/
444
+ ├── __init__.py # public API surface
445
+ ├── client.py # WareraClient
446
+ ├── sync.py # sync shim
447
+ ├── exceptions.py # error hierarchy
448
+ ├── _enums.py # all StrEnum classes from schema
449
+ ├── _http.py # httpx session, GET/POST encoding, retry
450
+ ├── _pagination.py # paginate(), collect_all()
451
+ ├── _batch.py # BatchSession, BatchItem, fetch_many_by_ids
452
+ ├── models/ # Pydantic response models (20 files)
453
+ └── resources/ # Resource classes (19 files)
454
+ ```
455
+
456
+ ---
457
+
458
+ ## Development
459
+
460
+ ```bash
461
+ git clone https://github.com/you/warera-client
462
+ cd warera-client
463
+ pip install -e ".[dev]"
464
+
465
+ # Unit tests (no API key needed)
466
+ pytest tests/unit/ -v
467
+
468
+ # Integration tests (live API)
469
+ WARERA_API_KEY=your_key pytest tests/integration/ -v
470
+
471
+ # Lint + type check
472
+ ruff check warera/
473
+ mypy warera/
474
+ ```
475
+
476
+ ---
477
+
478
+ ## License
479
+
480
+ MIT