polza-extras 0.2.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.
@@ -0,0 +1,9 @@
1
+ .venv/
2
+ __pycache__/
3
+ *.pyc
4
+ .pytest_cache/
5
+ .mypy_cache/
6
+ .ruff_cache/
7
+ dist/
8
+ .env
9
+ .coverage
@@ -0,0 +1,24 @@
1
+ # Changelog
2
+
3
+ ## 0.2.0 — 2026-10-24
4
+
5
+ First release on PyPI: `uv add polza-extras`.
6
+
7
+ - Tool calls: `ChatResult.tool_calls` (`ToolCall` with `id`, `name`, `arguments` and `parsed_arguments()`). A reply with tool calls and no text no longer raises `EmptyResponseError`.
8
+ - Streaming tool calls: `StreamEvent.tool_calls` passes `ToolCallDelta` pieces (`index`, `id`, `name`, `arguments` fragment) through as they arrive.
9
+ - Usage details: `Usage.cached_tokens`, `Usage.cache_write_tokens`, `Usage.reasoning_tokens` (0 when Polza does not send them), in `chat()` and in the last stream event.
10
+ - Structured output: `chat(..., json_schema=..., schema_name="response")` and the same in `chat_stream()`; takes a JSON Schema dict or a pydantic model class and sends a strict `json_schema` response format. Combining it with `json=True` or `response_format` raises `ValueError`.
11
+ - CI runs on Python 3.11–3.14, plus 3.15 pre-releases as experimental.
12
+ - Release workflow: tags `v*` are built with `uv build` and published to PyPI with Trusted Publishing.
13
+
14
+ ## 0.1.0 — 2026-10-24
15
+
16
+ First release. Not on PyPI yet; install from GitHub.
17
+
18
+ - `Polza` / `AsyncPolza` on top of the `openai` SDK, with `close()` and context managers.
19
+ - `chat()`: ruble cost (`cost_rub` as `Decimal`), reasoning text, JSON mode, Polza retry rules.
20
+ - `chat_stream()`: text and reasoning deltas, final usage and cost, `StreamError` for in-stream errors; abandoned streams are closed.
21
+ - Typed errors from Polza's error body (`AuthError`, `InsufficientFundsError`, `RateLimitError`, `NotFoundError`, …) with `trace_id` and the provider's real reason.
22
+ - `EmptyResponseError` when thinking used up `max_tokens`; it keeps usage, the raw response and the billed cost.
23
+ - Catalog: `models()`, `model()`, ruble pricing as `Decimal`, `Pricing.estimate`.
24
+ - Account: `balance()`, `key_info()`.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 polza-extras contributors
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,189 @@
1
+ Metadata-Version: 2.5
2
+ Name: polza-extras
3
+ Version: 0.2.0
4
+ Summary: Polza.ai helpers on top of the OpenAI SDK: ruble cost, typed errors, retry rules, model catalog, balance
5
+ Project-URL: Homepage, https://github.com/still-coding/polza-extras
6
+ Project-URL: Issues, https://github.com/still-coding/polza-extras/issues
7
+ Project-URL: Changelog, https://github.com/still-coding/polza-extras/blob/main/CHANGELOG.md
8
+ Author: still-coding
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: llm,openai,polza,polza.ai,rubles,russia
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Programming Language :: Python :: 3
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.11
22
+ Requires-Dist: httpx>=0.27
23
+ Requires-Dist: openai>=1.50
24
+ Requires-Dist: pydantic>=2.7
25
+ Description-Content-Type: text/markdown
26
+
27
+ # polza-extras
28
+
29
+ Small helpers for [Polza.ai](https://polza.ai) on top of the official `openai` SDK.
30
+
31
+ Polza is OpenAI-compatible, so for chat you can use the `openai` SDK directly. This library covers what the SDK doesn't know about:
32
+
33
+ - **Ruble cost** of each call (`usage.cost_rub`) as `Decimal`.
34
+ - **Typed errors** parsed from Polza's error body, with `trace_id` and the provider's real reason (`metadata.raw`), which is often hidden behind a generic «Некорректный запрос».
35
+ - **Polza's retry rules** (which statuses to retry, `Retry-After`, own vs provider rate limits).
36
+ - **Reasoning models**: a clear `EmptyResponseError` when thinking used up `max_tokens`, instead of an empty string.
37
+ - **Tool calls, usage details, structured output**: parsed `tool_calls`, cached/reasoning token counts, `json_schema=` with a dict or a pydantic model.
38
+ - **Catalog, balance and key info**: `models()`, `model()`, `balance()`, `key_info()`, with ruble prices as `Decimal` and a cost estimate.
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ uv add polza-extras # or: pip install polza-extras
44
+ ```
45
+
46
+ Python 3.11+. Dependencies: `openai`, `pydantic`.
47
+
48
+ ## Quickstart
49
+
50
+ ```python
51
+ from polza_extras import Polza
52
+
53
+ polza = Polza() # reads POLZA_AI_API_KEY; base URL https://polza.ai/api/v1
54
+
55
+ result = polza.chat(
56
+ "anthropic/claude-haiku-5.5", # always provider/model; a bare name is a 404
57
+ [{"role": "user", "content": "Привет!"}],
58
+ max_tokens=200,
59
+ reasoning={"type": "disabled"}, # any extra Polza field goes into the request body
60
+ )
61
+ print(result.text, result.cost_rub) # 'Привет! …' Decimal('0.0123')
62
+
63
+ data = polza.chat(model, messages, json=True) # response_format={"type": "json_object"}
64
+
65
+ polza.openai # the configured openai.OpenAI client, for anything else
66
+ polza.close() # or: with Polza() as polza: ...
67
+ ```
68
+
69
+ ### Tool calls
70
+
71
+ ```python
72
+ result = polza.chat(model, messages, tools=[weather_tool]) # tools pass through as is
73
+ for call in result.tool_calls: # [] for a plain text reply
74
+ call.id, call.name, call.arguments # arguments: the JSON string from the model
75
+ args = call.parsed_arguments() # dict; ValueError if not a JSON object
76
+ ```
77
+
78
+ A reply with tool calls usually has no text: `result.text == ""` and `finish_reason == "tool_calls"`. `EmptyResponseError` is raised only when there is neither text nor tool calls.
79
+
80
+ In a stream, `event.tool_calls` holds the raw `ToolCallDelta` pieces: the first piece of a call has `index`, `id` and `name`; join the `arguments` fragments of one `index` to get the full JSON.
81
+
82
+ ### Usage details
83
+
84
+ ```python
85
+ u = result.usage
86
+ u.prompt_tokens, u.completion_tokens, u.total_tokens
87
+ u.cached_tokens # prompt tokens read from the prompt cache
88
+ u.cache_write_tokens # prompt tokens written to the prompt cache
89
+ u.reasoning_tokens # completion tokens spent on thinking
90
+ ```
91
+
92
+ They are 0 when Polza does not report them. The last stream event carries the same `usage`.
93
+
94
+ ### Structured output
95
+
96
+ ```python
97
+ from pydantic import BaseModel
98
+
99
+ class City(BaseModel):
100
+ name: str
101
+ population_millions: float
102
+
103
+ result = polza.chat(model, messages, json_schema=City, schema_name="city")
104
+ city = City.model_validate_json(result.text)
105
+ ```
106
+
107
+ `json_schema` takes a pydantic model class or a JSON Schema dict and sends `response_format={"type": "json_schema", "json_schema": {"name": schema_name, "schema": ..., "strict": True}}`. Not every provider supports it: check `json_schema_strict` in the model's provider capabilities. Strict mode on some providers also wants `"additionalProperties": false` and every property in `required`; pass a dict when you need full control. Use either `json=True` (any JSON object) or `json_schema`, not both.
108
+
109
+ ### Streaming
110
+
111
+ ```python
112
+ for event in polza.chat_stream("anthropic/claude-haiku-5.5", messages, max_tokens=200):
113
+ print(event.delta, end="", flush=True) # event.reasoning holds thinking deltas, if any
114
+ print(event.cost_rub) # the last event carries usage and cost
115
+ ```
116
+
117
+ Errors before the first chunk are retried like `chat()`. An error event inside the stream raises `StreamError`; text already yielded stays yielded, and the request is not retried. Stopping early (`break`) closes the connection.
118
+
119
+ ### Async
120
+
121
+ `AsyncPolza` has the same methods, awaited (`async for` for `chat_stream`):
122
+
123
+ ```python
124
+ from polza_extras import AsyncPolza
125
+
126
+ polza = AsyncPolza()
127
+ result = await polza.chat("anthropic/claude-haiku-5.5", messages)
128
+ async for event in polza.chat_stream("anthropic/claude-haiku-5.5", messages):
129
+ ...
130
+ ```
131
+
132
+ ### Catalog, balance, key
133
+
134
+ ```python
135
+ page = polza.models(search="claude", type="chat", limit=10) # one catalog page
136
+ for m in page.items:
137
+ print(m.id, m.pricing.prompt_per_million, m.reasoning.mandatory)
138
+
139
+ sonnet = polza.model("anthropic/claude-sonnet-5.5") # NotFoundError for unknown ids
140
+ sonnet.pricing.estimate(prompt_tokens=1500, completion_tokens=80) # Decimal rubles
141
+
142
+ polza.balance() # Balance(amount, available, reserved, spent, updated_at)
143
+ polza.key_info() # KeyInfo(label, limit, limit_remaining, limit_reset, usage, usage_daily, ...)
144
+ ```
145
+
146
+ ## Errors
147
+
148
+ All errors subclass `PolzaError` (`status`, `code`, `message`, `trace_id`, `metadata`, `details`, `retry_after`).
149
+
150
+ | Status | Class | Retried by default |
151
+ |---|---|---|
152
+ | 401 | `AuthError` | no |
153
+ | 402 | `InsufficientFundsError` | no (opt-in `retry_402` for parallel-load reserve conflicts) |
154
+ | 403 | `ForbiddenError` | no |
155
+ | 404 | `NotFoundError` | no |
156
+ | 400, 413, other 4xx | `RequestError` | no |
157
+ | 429 with `Retry-After` | `RateLimitError(scope="own")` | yes, waits `Retry-After` (fails fast if above `max_wait`) |
158
+ | 429 without `Retry-After` | `RateLimitError(scope="provider")` | yes, backoff 1, 2, 4, 8 s |
159
+ | 408, 502 | `ServerError` | yes, backoff |
160
+ | 503 | `ServerError` | yes, honors `Retry-After` |
161
+ | connection failure | `PolzaConnectionError` | yes, backoff |
162
+ | 200 with no text and no tool calls | `EmptyResponseError` | no; carries `finish_reason`, `reasoning`, `usage`, `cost_rub` (still billed), `raw` |
163
+ | error event inside a stream | `StreamError` | no |
164
+
165
+ Polza doesn't charge for failed requests, so retrying is safe. Tune with `Polza(retry=RetryPolicy(max_retries=..., base_delay=..., max_wait=...))`.
166
+
167
+ ### Reasoning models
168
+
169
+ Thinking tokens count against `max_tokens`. If the model spends them all on thinking, `content` comes back empty and `chat()` raises `EmptyResponseError` with `finish_reason="length"`. Fixes:
170
+
171
+ - turn thinking off: `reasoning={"type": "disabled"}` (where allowed),
172
+ - lower it: `reasoning={"effort": "low"}`,
173
+ - raise `max_tokens`.
174
+
175
+ Some models (e.g. `anthropic/claude-sonnet-5.5`) make thinking mandatory; `model(id).reasoning.mandatory` tells you.
176
+
177
+ ## Development
178
+
179
+ ```bash
180
+ uv sync
181
+ uv run pytest # offline, against an in-process fake server; fails below 95% coverage
182
+ POLZA_AI_API_KEY=... uv run pytest -m live --no-cov # real API smoke tests, costs ~0.1 ₽
183
+ uv run ruff check src tests && uv run ruff format --check src tests && uv run pyright
184
+ ln -sf ../../scripts/pre-commit .git/hooks/pre-commit # all of the above before each commit
185
+ ```
186
+
187
+ ## License
188
+
189
+ MIT
@@ -0,0 +1,163 @@
1
+ # polza-extras
2
+
3
+ Small helpers for [Polza.ai](https://polza.ai) on top of the official `openai` SDK.
4
+
5
+ Polza is OpenAI-compatible, so for chat you can use the `openai` SDK directly. This library covers what the SDK doesn't know about:
6
+
7
+ - **Ruble cost** of each call (`usage.cost_rub`) as `Decimal`.
8
+ - **Typed errors** parsed from Polza's error body, with `trace_id` and the provider's real reason (`metadata.raw`), which is often hidden behind a generic «Некорректный запрос».
9
+ - **Polza's retry rules** (which statuses to retry, `Retry-After`, own vs provider rate limits).
10
+ - **Reasoning models**: a clear `EmptyResponseError` when thinking used up `max_tokens`, instead of an empty string.
11
+ - **Tool calls, usage details, structured output**: parsed `tool_calls`, cached/reasoning token counts, `json_schema=` with a dict or a pydantic model.
12
+ - **Catalog, balance and key info**: `models()`, `model()`, `balance()`, `key_info()`, with ruble prices as `Decimal` and a cost estimate.
13
+
14
+ ## Install
15
+
16
+ ```bash
17
+ uv add polza-extras # or: pip install polza-extras
18
+ ```
19
+
20
+ Python 3.11+. Dependencies: `openai`, `pydantic`.
21
+
22
+ ## Quickstart
23
+
24
+ ```python
25
+ from polza_extras import Polza
26
+
27
+ polza = Polza() # reads POLZA_AI_API_KEY; base URL https://polza.ai/api/v1
28
+
29
+ result = polza.chat(
30
+ "anthropic/claude-haiku-5.5", # always provider/model; a bare name is a 404
31
+ [{"role": "user", "content": "Привет!"}],
32
+ max_tokens=200,
33
+ reasoning={"type": "disabled"}, # any extra Polza field goes into the request body
34
+ )
35
+ print(result.text, result.cost_rub) # 'Привет! …' Decimal('0.0123')
36
+
37
+ data = polza.chat(model, messages, json=True) # response_format={"type": "json_object"}
38
+
39
+ polza.openai # the configured openai.OpenAI client, for anything else
40
+ polza.close() # or: with Polza() as polza: ...
41
+ ```
42
+
43
+ ### Tool calls
44
+
45
+ ```python
46
+ result = polza.chat(model, messages, tools=[weather_tool]) # tools pass through as is
47
+ for call in result.tool_calls: # [] for a plain text reply
48
+ call.id, call.name, call.arguments # arguments: the JSON string from the model
49
+ args = call.parsed_arguments() # dict; ValueError if not a JSON object
50
+ ```
51
+
52
+ A reply with tool calls usually has no text: `result.text == ""` and `finish_reason == "tool_calls"`. `EmptyResponseError` is raised only when there is neither text nor tool calls.
53
+
54
+ In a stream, `event.tool_calls` holds the raw `ToolCallDelta` pieces: the first piece of a call has `index`, `id` and `name`; join the `arguments` fragments of one `index` to get the full JSON.
55
+
56
+ ### Usage details
57
+
58
+ ```python
59
+ u = result.usage
60
+ u.prompt_tokens, u.completion_tokens, u.total_tokens
61
+ u.cached_tokens # prompt tokens read from the prompt cache
62
+ u.cache_write_tokens # prompt tokens written to the prompt cache
63
+ u.reasoning_tokens # completion tokens spent on thinking
64
+ ```
65
+
66
+ They are 0 when Polza does not report them. The last stream event carries the same `usage`.
67
+
68
+ ### Structured output
69
+
70
+ ```python
71
+ from pydantic import BaseModel
72
+
73
+ class City(BaseModel):
74
+ name: str
75
+ population_millions: float
76
+
77
+ result = polza.chat(model, messages, json_schema=City, schema_name="city")
78
+ city = City.model_validate_json(result.text)
79
+ ```
80
+
81
+ `json_schema` takes a pydantic model class or a JSON Schema dict and sends `response_format={"type": "json_schema", "json_schema": {"name": schema_name, "schema": ..., "strict": True}}`. Not every provider supports it: check `json_schema_strict` in the model's provider capabilities. Strict mode on some providers also wants `"additionalProperties": false` and every property in `required`; pass a dict when you need full control. Use either `json=True` (any JSON object) or `json_schema`, not both.
82
+
83
+ ### Streaming
84
+
85
+ ```python
86
+ for event in polza.chat_stream("anthropic/claude-haiku-5.5", messages, max_tokens=200):
87
+ print(event.delta, end="", flush=True) # event.reasoning holds thinking deltas, if any
88
+ print(event.cost_rub) # the last event carries usage and cost
89
+ ```
90
+
91
+ Errors before the first chunk are retried like `chat()`. An error event inside the stream raises `StreamError`; text already yielded stays yielded, and the request is not retried. Stopping early (`break`) closes the connection.
92
+
93
+ ### Async
94
+
95
+ `AsyncPolza` has the same methods, awaited (`async for` for `chat_stream`):
96
+
97
+ ```python
98
+ from polza_extras import AsyncPolza
99
+
100
+ polza = AsyncPolza()
101
+ result = await polza.chat("anthropic/claude-haiku-5.5", messages)
102
+ async for event in polza.chat_stream("anthropic/claude-haiku-5.5", messages):
103
+ ...
104
+ ```
105
+
106
+ ### Catalog, balance, key
107
+
108
+ ```python
109
+ page = polza.models(search="claude", type="chat", limit=10) # one catalog page
110
+ for m in page.items:
111
+ print(m.id, m.pricing.prompt_per_million, m.reasoning.mandatory)
112
+
113
+ sonnet = polza.model("anthropic/claude-sonnet-5.5") # NotFoundError for unknown ids
114
+ sonnet.pricing.estimate(prompt_tokens=1500, completion_tokens=80) # Decimal rubles
115
+
116
+ polza.balance() # Balance(amount, available, reserved, spent, updated_at)
117
+ polza.key_info() # KeyInfo(label, limit, limit_remaining, limit_reset, usage, usage_daily, ...)
118
+ ```
119
+
120
+ ## Errors
121
+
122
+ All errors subclass `PolzaError` (`status`, `code`, `message`, `trace_id`, `metadata`, `details`, `retry_after`).
123
+
124
+ | Status | Class | Retried by default |
125
+ |---|---|---|
126
+ | 401 | `AuthError` | no |
127
+ | 402 | `InsufficientFundsError` | no (opt-in `retry_402` for parallel-load reserve conflicts) |
128
+ | 403 | `ForbiddenError` | no |
129
+ | 404 | `NotFoundError` | no |
130
+ | 400, 413, other 4xx | `RequestError` | no |
131
+ | 429 with `Retry-After` | `RateLimitError(scope="own")` | yes, waits `Retry-After` (fails fast if above `max_wait`) |
132
+ | 429 without `Retry-After` | `RateLimitError(scope="provider")` | yes, backoff 1, 2, 4, 8 s |
133
+ | 408, 502 | `ServerError` | yes, backoff |
134
+ | 503 | `ServerError` | yes, honors `Retry-After` |
135
+ | connection failure | `PolzaConnectionError` | yes, backoff |
136
+ | 200 with no text and no tool calls | `EmptyResponseError` | no; carries `finish_reason`, `reasoning`, `usage`, `cost_rub` (still billed), `raw` |
137
+ | error event inside a stream | `StreamError` | no |
138
+
139
+ Polza doesn't charge for failed requests, so retrying is safe. Tune with `Polza(retry=RetryPolicy(max_retries=..., base_delay=..., max_wait=...))`.
140
+
141
+ ### Reasoning models
142
+
143
+ Thinking tokens count against `max_tokens`. If the model spends them all on thinking, `content` comes back empty and `chat()` raises `EmptyResponseError` with `finish_reason="length"`. Fixes:
144
+
145
+ - turn thinking off: `reasoning={"type": "disabled"}` (where allowed),
146
+ - lower it: `reasoning={"effort": "low"}`,
147
+ - raise `max_tokens`.
148
+
149
+ Some models (e.g. `anthropic/claude-sonnet-5.5`) make thinking mandatory; `model(id).reasoning.mandatory` tells you.
150
+
151
+ ## Development
152
+
153
+ ```bash
154
+ uv sync
155
+ uv run pytest # offline, against an in-process fake server; fails below 95% coverage
156
+ POLZA_AI_API_KEY=... uv run pytest -m live --no-cov # real API smoke tests, costs ~0.1 ₽
157
+ uv run ruff check src tests && uv run ruff format --check src tests && uv run pyright
158
+ ln -sf ../../scripts/pre-commit .git/hooks/pre-commit # all of the above before each commit
159
+ ```
160
+
161
+ ## License
162
+
163
+ MIT
@@ -0,0 +1,80 @@
1
+ [project]
2
+ name = "polza-extras"
3
+ version = "0.2.0"
4
+ description = "Polza.ai helpers on top of the OpenAI SDK: ruble cost, typed errors, retry rules, model catalog, balance"
5
+ readme = "README.md"
6
+ license = "MIT"
7
+ requires-python = ">=3.11"
8
+ authors = [{ name = "still-coding" }]
9
+ keywords = ["polza", "polza.ai", "openai", "llm", "russia", "rubles"]
10
+ classifiers = [
11
+ "Development Status :: 3 - Alpha",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.11",
16
+ "Programming Language :: Python :: 3.12",
17
+ "Programming Language :: Python :: 3.13",
18
+ "Programming Language :: Python :: 3.14",
19
+ "Typing :: Typed",
20
+ ]
21
+ dependencies = [
22
+ "openai>=1.50",
23
+ "httpx>=0.27",
24
+ "pydantic>=2.7",
25
+ ]
26
+
27
+ [dependency-groups]
28
+ dev = [
29
+ "pytest>=8",
30
+ "pytest-asyncio>=0.23",
31
+ "ruff>=0.6",
32
+ "pytest-cov>=7.1.0",
33
+ "pyright>=1.1.414",
34
+ ]
35
+
36
+ [build-system]
37
+ requires = ["hatchling"]
38
+ build-backend = "hatchling.build"
39
+
40
+ [project.urls]
41
+ Homepage = "https://github.com/still-coding/polza-extras"
42
+ Issues = "https://github.com/still-coding/polza-extras/issues"
43
+ Changelog = "https://github.com/still-coding/polza-extras/blob/main/CHANGELOG.md"
44
+
45
+ [tool.hatch.build.targets.sdist]
46
+ include = ["src", "tests", "README.md", "LICENSE", "CHANGELOG.md"]
47
+
48
+ [tool.hatch.build.targets.wheel]
49
+ packages = ["src/polza_extras"]
50
+
51
+ [tool.pytest.ini_options]
52
+ testpaths = ["tests"]
53
+ asyncio_mode = "auto"
54
+ markers = ["live: hits the real Polza API (needs POLZA_AI_API_KEY)"]
55
+ # Coverage is gated: plain `uv run pytest` (and CI) fails below fail_under.
56
+ # Live smoke tests cover little on purpose; run them with `--no-cov`.
57
+ addopts = "-m 'not live' --cov=polza_extras --cov-branch --cov-report=term-missing:skip-covered"
58
+
59
+ [tool.coverage.report]
60
+ fail_under = 95
61
+
62
+ # ruff replaces flake8 (E, W, F + bugbear B), isort (I) and black (`ruff format`).
63
+ [tool.ruff]
64
+ line-length = 100
65
+ target-version = "py311"
66
+ src = ["src", "tests"]
67
+
68
+ [tool.ruff.format]
69
+ exclude = ["*.md"] # README examples keep their aligned comments
70
+
71
+ [tool.ruff.lint]
72
+ select = ["E", "W", "F", "I", "B", "UP", "C4", "SIM"]
73
+
74
+ [tool.ruff.lint.isort]
75
+ known-first-party = ["polza_extras", "tests"]
76
+
77
+ [tool.pyright]
78
+ include = ["src"]
79
+ pythonVersion = "3.11"
80
+ typeCheckingMode = "strict"
@@ -0,0 +1,57 @@
1
+ from ._models import (
2
+ Balance,
3
+ ChatResult,
4
+ KeyInfo,
5
+ ModelInfo,
6
+ ModelPage,
7
+ Pricing,
8
+ ReasoningInfo,
9
+ StreamEvent,
10
+ ToolCall,
11
+ ToolCallDelta,
12
+ Usage,
13
+ )
14
+ from .client import DEFAULT_BASE_URL, AsyncPolza, Polza
15
+ from .errors import (
16
+ AuthError,
17
+ EmptyResponseError,
18
+ ForbiddenError,
19
+ InsufficientFundsError,
20
+ NotFoundError,
21
+ PolzaConnectionError,
22
+ PolzaError,
23
+ RateLimitError,
24
+ RequestError,
25
+ ServerError,
26
+ StreamError,
27
+ )
28
+ from .retry import RetryPolicy
29
+
30
+ __all__ = [
31
+ "DEFAULT_BASE_URL",
32
+ "AsyncPolza",
33
+ "AuthError",
34
+ "Balance",
35
+ "ChatResult",
36
+ "EmptyResponseError",
37
+ "ForbiddenError",
38
+ "InsufficientFundsError",
39
+ "KeyInfo",
40
+ "ModelInfo",
41
+ "ModelPage",
42
+ "NotFoundError",
43
+ "Polza",
44
+ "PolzaConnectionError",
45
+ "PolzaError",
46
+ "Pricing",
47
+ "RateLimitError",
48
+ "ReasoningInfo",
49
+ "RequestError",
50
+ "RetryPolicy",
51
+ "ServerError",
52
+ "StreamError",
53
+ "StreamEvent",
54
+ "ToolCall",
55
+ "ToolCallDelta",
56
+ "Usage",
57
+ ]
@@ -0,0 +1,13 @@
1
+ """Typed access to parsed JSON: containers get a known shape; a missing or wrong one is empty."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any, cast
6
+
7
+
8
+ def obj(value: Any) -> dict[str, Any]:
9
+ return cast("dict[str, Any]", value) if isinstance(value, dict) else {}
10
+
11
+
12
+ def arr(value: Any) -> list[Any]:
13
+ return cast("list[Any]", value) if isinstance(value, list) else []