valentina-python-client 1.2.1__tar.gz → 1.3.1__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.
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/PKG-INFO +13 -12
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/README.md +11 -11
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/pyproject.toml +7 -27
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/__init__.py +24 -2
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/client.py +24 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/config.py +3 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/constants.py +2 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/campaigns.py +6 -5
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/character_blueprint.py +1 -1
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/characters.py +38 -27
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/companies.py +4 -3
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/diceroll.py +7 -7
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/dictionary.py +2 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/shared.py +8 -5
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/users.py +5 -4
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/registry.py +15 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/base.py +123 -57
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/LICENSE +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/endpoints.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/exceptions.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/__init__.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/books.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/chapters.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/character_autogen.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/character_trait.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/developers.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/global_admin.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/pagination.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/system.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/py.typed +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/__init__.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/campaign_book_chapters.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/campaign_books.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/campaigns.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/character_autogen.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/character_blueprint.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/character_traits.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/characters.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/companies.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/developers.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/dicerolls.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/dictionary.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/global_admin.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/options.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/system.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/users.py +0 -0
- {valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/validate_constants.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.3
|
|
2
2
|
Name: valentina-python-client
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.1
|
|
4
4
|
Summary: Async Python client library for the Valentina Noir API
|
|
5
5
|
Author: Nate Landau
|
|
6
6
|
Author-email: Nate Landau <github@natenate.org>
|
|
@@ -17,6 +17,7 @@ Classifier: Programming Language :: Python :: 3.13
|
|
|
17
17
|
Classifier: Programming Language :: Python :: 3.14
|
|
18
18
|
Requires-Dist: anyio>=4.12.1
|
|
19
19
|
Requires-Dist: httpx>=0.28.1
|
|
20
|
+
Requires-Dist: loguru>=0.7.3
|
|
20
21
|
Requires-Dist: pydantic[email]>=2.12.5
|
|
21
22
|
Requires-Python: >=3.13
|
|
22
23
|
Project-URL: Homepage, https://docs.valentina-noir.com/python-api-client/
|
|
@@ -29,13 +30,13 @@ Async Python client library for accessing the Valentina Noir API.
|
|
|
29
30
|
|
|
30
31
|
## Features
|
|
31
32
|
|
|
32
|
-
-
|
|
33
|
-
-
|
|
34
|
-
-
|
|
35
|
-
-
|
|
36
|
-
-
|
|
37
|
-
-
|
|
38
|
-
-
|
|
33
|
+
- **Async-first design** - Built on httpx for efficient async HTTP operations
|
|
34
|
+
- **Type-safe** - Full type hints with Pydantic models for request/response validation
|
|
35
|
+
- **Convenient factory pattern** - Create a client once, access services from anywhere
|
|
36
|
+
- **Automatic pagination** - Stream through large datasets with `iter_all()` or fetch everything with `list_all()`
|
|
37
|
+
- **Robust error handling** - Specific exception types for different error conditions
|
|
38
|
+
- **Idempotency support** - Optional automatic idempotency keys for safe retries
|
|
39
|
+
- **Rate limit handling** - Built-in support for automatic rate limit retries
|
|
39
40
|
|
|
40
41
|
This client is a supported and up-to-date reference implementation for the Valentina Noir API. The full documentation for is available at https://docs.valentina-noir.com/python-api-client/.
|
|
41
42
|
|
|
@@ -61,12 +62,12 @@ The script reads configuration from (highest precedence first):
|
|
|
61
62
|
|
|
62
63
|
1. CLI arguments (`--api-url`, `--api-key`, `--company-id`)
|
|
63
64
|
2. System environment variables (`VALENTINA_CLIENT_BASE_URL`, `VALENTINA_CLIENT_API_KEY`, `VALENTINA_CLIENT_DEFAULT_COMPANY_ID`)
|
|
64
|
-
3. A `.env.
|
|
65
|
+
3. A `.env.secret` file in the project root
|
|
65
66
|
|
|
66
67
|
Exit codes: `0` = all constants match, `1` = mismatches found, `2` = missing configuration.
|
|
67
68
|
|
|
68
69
|
## Resources
|
|
69
70
|
|
|
70
|
-
-
|
|
71
|
-
-
|
|
72
|
-
-
|
|
71
|
+
- [Full Client Documentation](https://docs.valentina-noir.com/python-api-client/)
|
|
72
|
+
- [API Concepts](https://docs.valentina-noir.com/concepts/)
|
|
73
|
+
- [API Reference](https://api.valentina-noir.com/docs)
|
|
@@ -4,13 +4,13 @@ Async Python client library for accessing the Valentina Noir API.
|
|
|
4
4
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
|
-
-
|
|
8
|
-
-
|
|
9
|
-
-
|
|
10
|
-
-
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
7
|
+
- **Async-first design** - Built on httpx for efficient async HTTP operations
|
|
8
|
+
- **Type-safe** - Full type hints with Pydantic models for request/response validation
|
|
9
|
+
- **Convenient factory pattern** - Create a client once, access services from anywhere
|
|
10
|
+
- **Automatic pagination** - Stream through large datasets with `iter_all()` or fetch everything with `list_all()`
|
|
11
|
+
- **Robust error handling** - Specific exception types for different error conditions
|
|
12
|
+
- **Idempotency support** - Optional automatic idempotency keys for safe retries
|
|
13
|
+
- **Rate limit handling** - Built-in support for automatic rate limit retries
|
|
14
14
|
|
|
15
15
|
This client is a supported and up-to-date reference implementation for the Valentina Noir API. The full documentation for is available at https://docs.valentina-noir.com/python-api-client/.
|
|
16
16
|
|
|
@@ -36,12 +36,12 @@ The script reads configuration from (highest precedence first):
|
|
|
36
36
|
|
|
37
37
|
1. CLI arguments (`--api-url`, `--api-key`, `--company-id`)
|
|
38
38
|
2. System environment variables (`VALENTINA_CLIENT_BASE_URL`, `VALENTINA_CLIENT_API_KEY`, `VALENTINA_CLIENT_DEFAULT_COMPANY_ID`)
|
|
39
|
-
3. A `.env.
|
|
39
|
+
3. A `.env.secret` file in the project root
|
|
40
40
|
|
|
41
41
|
Exit codes: `0` = all constants match, `1` = mismatches found, `2` = missing configuration.
|
|
42
42
|
|
|
43
43
|
## Resources
|
|
44
44
|
|
|
45
|
-
-
|
|
46
|
-
-
|
|
47
|
-
-
|
|
45
|
+
- [Full Client Documentation](https://docs.valentina-noir.com/python-api-client/)
|
|
46
|
+
- [API Concepts](https://docs.valentina-noir.com/concepts/)
|
|
47
|
+
- [API Reference](https://api.valentina-noir.com/docs)
|
|
@@ -4,13 +4,13 @@
|
|
|
4
4
|
"Programming Language :: Python :: 3.13",
|
|
5
5
|
"Programming Language :: Python :: 3.14",
|
|
6
6
|
]
|
|
7
|
-
dependencies = ["anyio>=4.12.1", "httpx>=0.28.1", "pydantic[email]>=2.12.5"]
|
|
7
|
+
dependencies = ["anyio>=4.12.1", "httpx>=0.28.1", "loguru>=0.7.3", "pydantic[email]>=2.12.5"]
|
|
8
8
|
description = "Async Python client library for the Valentina Noir API"
|
|
9
9
|
license = { file = "LICENSE" }
|
|
10
10
|
name = "valentina-python-client"
|
|
11
11
|
readme = "README.md"
|
|
12
12
|
requires-python = ">=3.13"
|
|
13
|
-
version = "1.
|
|
13
|
+
version = "1.3.1"
|
|
14
14
|
|
|
15
15
|
[project.urls]
|
|
16
16
|
Homepage = "https://docs.valentina-noir.com/python-api-client/"
|
|
@@ -25,10 +25,10 @@
|
|
|
25
25
|
|
|
26
26
|
[dependency-groups]
|
|
27
27
|
dev = [
|
|
28
|
-
"commitizen>=4.13.
|
|
28
|
+
"commitizen>=4.13.8",
|
|
29
29
|
"coverage>=7.13.4",
|
|
30
30
|
"duty>=1.9.0",
|
|
31
|
-
"prek>=0.3.
|
|
31
|
+
"prek>=0.3.3",
|
|
32
32
|
"pytest-anyio>=0.0.0",
|
|
33
33
|
"pytest-clarity>=1.0.1",
|
|
34
34
|
"pytest-cov>=7.0.0",
|
|
@@ -39,10 +39,10 @@
|
|
|
39
39
|
"pytest-xdist>=3.8.0",
|
|
40
40
|
"pytest>=9.0.2",
|
|
41
41
|
"respx>=0.22.0",
|
|
42
|
-
"ruff>=0.15.
|
|
42
|
+
"ruff>=0.15.2",
|
|
43
43
|
"shellcheck-py>=0.11.0.1",
|
|
44
44
|
"ty>=0.0.17",
|
|
45
|
-
"typos>=1.43.
|
|
45
|
+
"typos>=1.43.5",
|
|
46
46
|
"vulture>=2.14",
|
|
47
47
|
"yamllint>=1.38.0",
|
|
48
48
|
]
|
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
changelog_merge_prerelease = true
|
|
53
53
|
tag_format = "v$version"
|
|
54
54
|
update_changelog_on_bump = true
|
|
55
|
-
|
|
55
|
+
version_files = ["src/vclient/__init__.py:__version__"]
|
|
56
56
|
version_provider = "uv"
|
|
57
57
|
|
|
58
58
|
[tool.coverage.report] # https://coverage.readthedocs.io/en/latest/config.html#report
|
|
@@ -80,26 +80,6 @@
|
|
|
80
80
|
[tool.coverage.xml]
|
|
81
81
|
output = ".cache/coverage.xml"
|
|
82
82
|
|
|
83
|
-
[tool.mypy] # https://mypy.readthedocs.io/en/latest/config_file.html
|
|
84
|
-
cache_dir = ".cache/mypy"
|
|
85
|
-
disallow_any_unimported = false
|
|
86
|
-
disallow_subclassing_any = false
|
|
87
|
-
disallow_untyped_decorators = false
|
|
88
|
-
disallow_untyped_defs = true
|
|
89
|
-
exclude = ['duties.py', 'tests/']
|
|
90
|
-
follow_imports = "normal"
|
|
91
|
-
ignore_missing_imports = true
|
|
92
|
-
junit_xml = ".cache/mypy.xml"
|
|
93
|
-
no_implicit_optional = true
|
|
94
|
-
pretty = false
|
|
95
|
-
show_column_numbers = true
|
|
96
|
-
show_error_codes = true
|
|
97
|
-
show_error_context = true
|
|
98
|
-
strict_optional = false
|
|
99
|
-
warn_redundant_casts = true
|
|
100
|
-
warn_unreachable = true
|
|
101
|
-
warn_unused_ignores = true
|
|
102
|
-
|
|
103
83
|
[tool.pytest.ini_options]
|
|
104
84
|
|
|
105
85
|
addopts = "--color=yes --doctest-modules --strict-config --strict-markers -n auto --dist loadfile"
|
|
@@ -11,8 +11,28 @@ For models, use: from vclient.models import Character, Campaign, ...
|
|
|
11
11
|
For service classes, use: from vclient.services import CharactersService, ...
|
|
12
12
|
"""
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
14
|
+
import logging as _logging
|
|
15
|
+
|
|
16
|
+
from loguru import logger as _logger
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class _PropagateHandler(_logging.Handler):
|
|
20
|
+
"""Forward loguru messages to stdlib logging for caplog/handler compatibility."""
|
|
21
|
+
|
|
22
|
+
def emit(self, record: _logging.LogRecord) -> None:
|
|
23
|
+
_logging.getLogger(record.name).handle(record)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
_logger.add(
|
|
27
|
+
_PropagateHandler(),
|
|
28
|
+
format="{message}",
|
|
29
|
+
filter=lambda record: record["name"].startswith("vclient"),
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
_logger.disable("vclient")
|
|
33
|
+
|
|
34
|
+
from vclient.client import VClient # noqa: E402
|
|
35
|
+
from vclient.registry import ( # noqa: E402
|
|
16
36
|
books_service,
|
|
17
37
|
campaigns_service,
|
|
18
38
|
chapters_service,
|
|
@@ -50,3 +70,5 @@ __all__ = (
|
|
|
50
70
|
"system_service",
|
|
51
71
|
"users_service",
|
|
52
72
|
)
|
|
73
|
+
|
|
74
|
+
__version__ = "1.3.1"
|
|
@@ -1,16 +1,19 @@
|
|
|
1
1
|
"""Main API client for Valentina."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
+
import platform
|
|
4
5
|
from types import TracebackType
|
|
5
6
|
from typing import TYPE_CHECKING, Self
|
|
6
7
|
|
|
7
8
|
import httpx
|
|
9
|
+
from loguru import logger
|
|
8
10
|
|
|
9
11
|
from vclient.config import _APIConfig
|
|
10
12
|
from vclient.constants import (
|
|
11
13
|
API_KEY_HEADER,
|
|
12
14
|
DEFAULT_MAX_RETRIES,
|
|
13
15
|
DEFAULT_RETRY_DELAY,
|
|
16
|
+
DEFAULT_RETRY_STATUSES,
|
|
14
17
|
DEFAULT_TIMEOUT,
|
|
15
18
|
ENV_API_KEY,
|
|
16
19
|
ENV_BASE_URL,
|
|
@@ -76,6 +79,7 @@ class VClient:
|
|
|
76
79
|
retry_delay: float = DEFAULT_RETRY_DELAY,
|
|
77
80
|
auto_retry_rate_limit: bool = True,
|
|
78
81
|
auto_idempotency_keys: bool = False,
|
|
82
|
+
retry_statuses: set[int] | frozenset[int] | None = None,
|
|
79
83
|
default_company_id: str | None = None,
|
|
80
84
|
headers: dict[str, str] | None = None,
|
|
81
85
|
set_as_default: bool = True,
|
|
@@ -100,6 +104,8 @@ class VClient:
|
|
|
100
104
|
auto_retry_rate_limit: Automatically retry requests that hit rate limits.
|
|
101
105
|
auto_idempotency_keys: Automatically generate idempotency keys for
|
|
102
106
|
POST/PUT/PATCH requests.
|
|
107
|
+
retry_statuses: HTTP status codes that trigger automatic retries.
|
|
108
|
+
Defaults to {429, 500, 502, 503, 504}.
|
|
103
109
|
default_company_id: Default company ID to use when not explicitly provided
|
|
104
110
|
to service factory methods. Falls back to
|
|
105
111
|
VALENTINA_CLIENT_DEFAULT_COMPANY_ID.
|
|
@@ -132,6 +138,9 @@ class VClient:
|
|
|
132
138
|
retry_delay=retry_delay,
|
|
133
139
|
auto_retry_rate_limit=auto_retry_rate_limit,
|
|
134
140
|
auto_idempotency_keys=auto_idempotency_keys,
|
|
141
|
+
retry_statuses=frozenset(retry_statuses)
|
|
142
|
+
if retry_statuses is not None
|
|
143
|
+
else DEFAULT_RETRY_STATUSES,
|
|
135
144
|
default_company_id=resolved_company_id,
|
|
136
145
|
headers=headers or {},
|
|
137
146
|
)
|
|
@@ -147,11 +156,22 @@ class VClient:
|
|
|
147
156
|
|
|
148
157
|
configure_default_client(self)
|
|
149
158
|
|
|
159
|
+
logger.bind(
|
|
160
|
+
base_url=self._config.base_url,
|
|
161
|
+
timeout=self._config.timeout,
|
|
162
|
+
max_retries=self._config.max_retries,
|
|
163
|
+
).info("Initialize VClient")
|
|
164
|
+
|
|
150
165
|
def _create_http_client(self) -> httpx.AsyncClient:
|
|
151
166
|
"""Create and configure the HTTP client."""
|
|
167
|
+
from vclient import __version__
|
|
168
|
+
|
|
169
|
+
user_agent = f"vclient/{__version__} Python/{platform.python_version()}"
|
|
170
|
+
|
|
152
171
|
headers = {
|
|
153
172
|
"Accept": "application/json",
|
|
154
173
|
"Content-Type": "application/json",
|
|
174
|
+
"User-Agent": user_agent,
|
|
155
175
|
**self._config.headers,
|
|
156
176
|
}
|
|
157
177
|
|
|
@@ -179,7 +199,11 @@ class VClient:
|
|
|
179
199
|
|
|
180
200
|
async def close(self) -> None:
|
|
181
201
|
"""Close the HTTP client and release resources."""
|
|
202
|
+
from vclient.registry import clear_default_client
|
|
203
|
+
|
|
204
|
+
logger.bind(base_url=self._config.base_url).info("Close VClient")
|
|
182
205
|
await self._http.aclose()
|
|
206
|
+
clear_default_client(self)
|
|
183
207
|
|
|
184
208
|
@property
|
|
185
209
|
def is_closed(self) -> bool:
|
|
@@ -5,6 +5,7 @@ from dataclasses import dataclass, field
|
|
|
5
5
|
from vclient.constants import (
|
|
6
6
|
DEFAULT_MAX_RETRIES,
|
|
7
7
|
DEFAULT_RETRY_DELAY,
|
|
8
|
+
DEFAULT_RETRY_STATUSES,
|
|
8
9
|
DEFAULT_TIMEOUT,
|
|
9
10
|
)
|
|
10
11
|
|
|
@@ -23,6 +24,7 @@ class _APIConfig:
|
|
|
23
24
|
retry_delay: Base delay between retries in seconds.
|
|
24
25
|
auto_retry_rate_limit: Automatically retry requests that hit rate limits (429).
|
|
25
26
|
auto_idempotency_keys: Automatically generate idempotency keys for POST/PUT/PATCH.
|
|
27
|
+
retry_statuses: HTTP status codes that trigger automatic retries.
|
|
26
28
|
default_company_id: Default company ID to use when not explicitly provided.
|
|
27
29
|
"""
|
|
28
30
|
|
|
@@ -33,6 +35,7 @@ class _APIConfig:
|
|
|
33
35
|
retry_delay: float = DEFAULT_RETRY_DELAY
|
|
34
36
|
auto_retry_rate_limit: bool = True
|
|
35
37
|
auto_idempotency_keys: bool = False
|
|
38
|
+
retry_statuses: frozenset[int] = DEFAULT_RETRY_STATUSES
|
|
36
39
|
default_company_id: str | None = None
|
|
37
40
|
headers: dict[str, str] = field(default_factory=dict)
|
|
38
41
|
|
|
@@ -14,6 +14,8 @@ ENV_DEFAULT_COMPANY_ID = "VALENTINA_CLIENT_DEFAULT_COMPANY_ID"
|
|
|
14
14
|
DEFAULT_TIMEOUT = 30.0
|
|
15
15
|
DEFAULT_MAX_RETRIES = 3
|
|
16
16
|
DEFAULT_RETRY_DELAY = 1.0
|
|
17
|
+
DEFAULT_RETRY_STATUSES: frozenset[int] = frozenset({429, 500, 502, 503, 504})
|
|
18
|
+
IDEMPOTENT_HTTP_METHODS: frozenset[str] = frozenset({"GET", "PUT", "DELETE"})
|
|
17
19
|
|
|
18
20
|
# Pagination defaults
|
|
19
21
|
DEFAULT_PAGE_LIMIT = 10
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/campaigns.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Pydantic models for Campaign API responses and requests."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
+
from typing import Annotated
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
@@ -39,7 +40,7 @@ class CampaignCreate(BaseModel):
|
|
|
39
40
|
"""
|
|
40
41
|
|
|
41
42
|
name: str = Field(min_length=3, max_length=50)
|
|
42
|
-
description: str | None =
|
|
43
|
+
description: Annotated[str, Field(min_length=3)] | None = None
|
|
43
44
|
desperation: int = Field(default=0, ge=0, le=5)
|
|
44
45
|
danger: int = Field(default=0, ge=0, le=5)
|
|
45
46
|
|
|
@@ -50,10 +51,10 @@ class CampaignUpdate(BaseModel):
|
|
|
50
51
|
Only include fields that need to be changed; omitted fields remain unchanged.
|
|
51
52
|
"""
|
|
52
53
|
|
|
53
|
-
name: str
|
|
54
|
-
description: str | None =
|
|
55
|
-
desperation: int
|
|
56
|
-
danger: int
|
|
54
|
+
name: Annotated[str, Field(min_length=3, max_length=50)] | None = None
|
|
55
|
+
description: Annotated[str, Field(min_length=3)] | None = None
|
|
56
|
+
desperation: Annotated[int, Field(ge=0, le=5)] | None = None
|
|
57
|
+
danger: Annotated[int, Field(ge=0, le=5)] | None = None
|
|
57
58
|
|
|
58
59
|
|
|
59
60
|
__all__ = [
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/characters.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Pydantic models for Character API responses and requests."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import
|
|
4
|
+
from typing import Annotated
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
@@ -29,8 +29,10 @@ class VampireAttributes(BaseModel):
|
|
|
29
29
|
clan_name: str | None = Field(default=None, description="Name of the vampire clan.")
|
|
30
30
|
generation: int | None = Field(default=None, description="Vampire generation.")
|
|
31
31
|
sire: str | None = Field(default=None, description="Name of the vampire's sire.")
|
|
32
|
-
bane:
|
|
33
|
-
compulsion:
|
|
32
|
+
bane: NameDescriptionSubDocument | None = Field(default=None, description="Clan bane details.")
|
|
33
|
+
compulsion: NameDescriptionSubDocument | None = Field(
|
|
34
|
+
default=None, description="Clan compulsion details."
|
|
35
|
+
)
|
|
34
36
|
|
|
35
37
|
|
|
36
38
|
class VampireAttributesCreate(BaseModel):
|
|
@@ -63,6 +65,7 @@ class WerewolfAttributes(BaseModel):
|
|
|
63
65
|
pack_name: str | None = Field(default=None, description="Name of the werewolf's pack.")
|
|
64
66
|
rite_ids: list[str] = Field(default_factory=list, description="List of werewolf rite IDs.")
|
|
65
67
|
gift_ids: list[str] = Field(default_factory=list, description="List of werewolf gift IDs.")
|
|
68
|
+
total_renown: int = Field(default=0, description="Total renown.")
|
|
66
69
|
|
|
67
70
|
|
|
68
71
|
class WerewolfAttributesCreate(BaseModel):
|
|
@@ -153,20 +156,22 @@ class Character(BaseModel):
|
|
|
153
156
|
# Identity
|
|
154
157
|
name_first: str = Field(..., min_length=3, description="Character's first name.")
|
|
155
158
|
name_last: str = Field(..., min_length=3, description="Character's last name.")
|
|
156
|
-
name_nick: str | None = Field(
|
|
157
|
-
default=None,
|
|
159
|
+
name_nick: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
160
|
+
default=None, description="Character's nickname."
|
|
158
161
|
)
|
|
159
162
|
name: str = Field(..., description="Character's display name.")
|
|
160
163
|
name_full: str = Field(..., description="Character's full name.")
|
|
161
164
|
|
|
162
165
|
# Biography
|
|
163
166
|
age: int | None = Field(default=None, description="Character's age.")
|
|
164
|
-
biography: str | None = Field(
|
|
165
|
-
|
|
166
|
-
|
|
167
|
+
biography: Annotated[str, Field(min_length=3)] | None = Field(
|
|
168
|
+
default=None, description="Character biography."
|
|
169
|
+
)
|
|
170
|
+
demeanor: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
171
|
+
default=None, description="Character's demeanor."
|
|
167
172
|
)
|
|
168
|
-
nature: str | None = Field(
|
|
169
|
-
default=None,
|
|
173
|
+
nature: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
174
|
+
default=None, description="Character's nature."
|
|
170
175
|
)
|
|
171
176
|
concept_id: str | None = Field(default=None, description="ID of the character concept.")
|
|
172
177
|
|
|
@@ -218,16 +223,18 @@ class CharacterCreate(BaseModel):
|
|
|
218
223
|
|
|
219
224
|
# Optional fields
|
|
220
225
|
type: CharacterType | None = Field(default=None, description="Character type.")
|
|
221
|
-
name_nick: str | None = Field(
|
|
222
|
-
default=None,
|
|
226
|
+
name_nick: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
227
|
+
default=None, description="Character's nickname."
|
|
223
228
|
)
|
|
224
229
|
age: int | None = Field(default=None, description="Character's age.")
|
|
225
|
-
biography: str | None = Field(
|
|
226
|
-
|
|
227
|
-
|
|
230
|
+
biography: Annotated[str, Field(min_length=3)] | None = Field(
|
|
231
|
+
default=None, description="Character biography."
|
|
232
|
+
)
|
|
233
|
+
demeanor: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
234
|
+
default=None, description="Character's demeanor."
|
|
228
235
|
)
|
|
229
|
-
nature: str | None = Field(
|
|
230
|
-
default=None,
|
|
236
|
+
nature: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
237
|
+
default=None, description="Character's nature."
|
|
231
238
|
)
|
|
232
239
|
concept_id: str | None = Field(default=None, description="ID of the character concept.")
|
|
233
240
|
user_player_id: str | None = Field(
|
|
@@ -261,21 +268,25 @@ class CharacterUpdate(BaseModel):
|
|
|
261
268
|
game_version: GameVersion | None = None
|
|
262
269
|
status: CharacterStatus | None = None
|
|
263
270
|
|
|
264
|
-
name_first: str | None = Field(
|
|
265
|
-
default=None,
|
|
271
|
+
name_first: Annotated[str, Field(min_length=3)] | None = Field(
|
|
272
|
+
default=None, description="Character's first name."
|
|
266
273
|
)
|
|
267
|
-
name_last: str | None = Field(
|
|
268
|
-
|
|
269
|
-
|
|
274
|
+
name_last: Annotated[str, Field(min_length=3)] | None = Field(
|
|
275
|
+
default=None, description="Character's last name."
|
|
276
|
+
)
|
|
277
|
+
name_nick: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
278
|
+
default=None, description="Character's nickname."
|
|
270
279
|
)
|
|
271
280
|
|
|
272
281
|
age: int | None = None
|
|
273
|
-
biography: str | None = Field(
|
|
274
|
-
|
|
275
|
-
|
|
282
|
+
biography: Annotated[str, Field(min_length=3)] | None = Field(
|
|
283
|
+
default=None, description="Character biography."
|
|
284
|
+
)
|
|
285
|
+
demeanor: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
286
|
+
default=None, description="Character's demeanor."
|
|
276
287
|
)
|
|
277
|
-
nature: str | None = Field(
|
|
278
|
-
default=None,
|
|
288
|
+
nature: Annotated[str, Field(min_length=3, max_length=50)] | None = Field(
|
|
289
|
+
default=None, description="Character's nature."
|
|
279
290
|
)
|
|
280
291
|
concept_id: str | None = Field(default=None, description="ID of the character concept.")
|
|
281
292
|
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/companies.py
RENAMED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Pydantic models for Company API responses."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
+
from typing import Annotated
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
@@ -77,7 +78,7 @@ class CompanyCreate(BaseModel):
|
|
|
77
78
|
|
|
78
79
|
name: str = Field(min_length=3, max_length=50)
|
|
79
80
|
email: str
|
|
80
|
-
description: str | None =
|
|
81
|
+
description: Annotated[str, Field(min_length=3)] | None = None
|
|
81
82
|
settings: CompanySettings | None = None
|
|
82
83
|
|
|
83
84
|
|
|
@@ -87,9 +88,9 @@ class CompanyUpdate(BaseModel):
|
|
|
87
88
|
Only include fields that need to be changed; omitted fields remain unchanged.
|
|
88
89
|
"""
|
|
89
90
|
|
|
90
|
-
name: str
|
|
91
|
+
name: Annotated[str, Field(min_length=3, max_length=50)] | None = None
|
|
91
92
|
email: str | None = None
|
|
92
|
-
description: str | None =
|
|
93
|
+
description: Annotated[str, Field(min_length=3)] | None = None
|
|
93
94
|
settings: CompanySettings | None = None
|
|
94
95
|
|
|
95
96
|
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/diceroll.py
RENAMED
|
@@ -12,16 +12,16 @@ class DiceRollResultSchema(BaseModel):
|
|
|
12
12
|
|
|
13
13
|
total_result: int | None = None
|
|
14
14
|
total_result_type: RollResultType
|
|
15
|
-
total_result_humanized: str
|
|
15
|
+
total_result_humanized: str
|
|
16
16
|
total_dice_roll: list[int] = Field(default_factory=list)
|
|
17
17
|
player_roll: list[int] = Field(default_factory=list)
|
|
18
18
|
desperation_roll: list[int] = Field(default_factory=list)
|
|
19
|
-
total_dice_roll_emoji: str
|
|
20
|
-
total_dice_roll_shortcode: str
|
|
21
|
-
player_roll_emoji: str
|
|
22
|
-
player_roll_shortcode: str
|
|
23
|
-
desperation_roll_emoji: str
|
|
24
|
-
desperation_roll_shortcode: str
|
|
19
|
+
total_dice_roll_emoji: str
|
|
20
|
+
total_dice_roll_shortcode: str
|
|
21
|
+
player_roll_emoji: str
|
|
22
|
+
player_roll_shortcode: str
|
|
23
|
+
desperation_roll_emoji: str
|
|
24
|
+
desperation_roll_shortcode: str
|
|
25
25
|
|
|
26
26
|
|
|
27
27
|
class Diceroll(BaseModel):
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/shared.py
RENAMED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Shared Pydantic models used across multiple services."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
-
from typing import Any
|
|
4
|
+
from typing import Annotated, Any
|
|
5
5
|
|
|
6
6
|
from pydantic import BaseModel, Field
|
|
7
7
|
|
|
@@ -40,11 +40,14 @@ class S3Asset(BaseModel):
|
|
|
40
40
|
id: str
|
|
41
41
|
date_created: datetime
|
|
42
42
|
date_modified: datetime
|
|
43
|
-
|
|
43
|
+
asset_type: S3AssetType
|
|
44
|
+
mime_type: str
|
|
44
45
|
original_filename: str
|
|
45
46
|
public_url: str
|
|
46
47
|
uploaded_by: str
|
|
48
|
+
company_id: str
|
|
47
49
|
parent_type: S3AssetParentType | None = None
|
|
50
|
+
parent_id: str | None = None
|
|
48
51
|
|
|
49
52
|
|
|
50
53
|
# -----------------------------------------------------------------------------
|
|
@@ -81,8 +84,8 @@ class NoteUpdate(BaseModel):
|
|
|
81
84
|
Only include fields that need to be changed; omitted fields remain unchanged.
|
|
82
85
|
"""
|
|
83
86
|
|
|
84
|
-
title: str
|
|
85
|
-
content: str | None =
|
|
87
|
+
title: Annotated[str, Field(min_length=3, max_length=50)] | None = None
|
|
88
|
+
content: Annotated[str, Field(min_length=3)] | None = None
|
|
86
89
|
|
|
87
90
|
|
|
88
91
|
# -----------------------------------------------------------------------------
|
|
@@ -190,7 +193,7 @@ class CharacterSpecialty(BaseModel):
|
|
|
190
193
|
|
|
191
194
|
name: str
|
|
192
195
|
type: SpecialtyType
|
|
193
|
-
description: str
|
|
196
|
+
description: str
|
|
194
197
|
|
|
195
198
|
|
|
196
199
|
__all__ = [
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Pydantic models for User API responses and requests."""
|
|
2
2
|
|
|
3
3
|
from datetime import datetime
|
|
4
|
+
from typing import Annotated
|
|
4
5
|
|
|
5
6
|
from pydantic import BaseModel, Field
|
|
6
7
|
|
|
@@ -89,7 +90,7 @@ class UserUpdate(BaseModel):
|
|
|
89
90
|
Only include fields that need to be changed; omitted fields remain unchanged.
|
|
90
91
|
"""
|
|
91
92
|
|
|
92
|
-
name: str
|
|
93
|
+
name: Annotated[str, Field(min_length=3, max_length=50)] | None = None
|
|
93
94
|
email: str | None = None
|
|
94
95
|
role: UserRole | None = None
|
|
95
96
|
discord_profile: DiscordProfile | None = None
|
|
@@ -124,7 +125,7 @@ class QuickrollCreate(BaseModel):
|
|
|
124
125
|
"""
|
|
125
126
|
|
|
126
127
|
name: str = Field(min_length=3, max_length=50)
|
|
127
|
-
description: str | None =
|
|
128
|
+
description: Annotated[str, Field(min_length=3)] | None = None
|
|
128
129
|
trait_ids: list[str] = Field(default_factory=list)
|
|
129
130
|
|
|
130
131
|
|
|
@@ -134,8 +135,8 @@ class QuickrollUpdate(BaseModel):
|
|
|
134
135
|
Only include fields that need to be changed; omitted fields remain unchanged.
|
|
135
136
|
"""
|
|
136
137
|
|
|
137
|
-
name: str
|
|
138
|
-
description: str | None =
|
|
138
|
+
name: Annotated[str, Field(min_length=3, max_length=50)] | None = None
|
|
139
|
+
description: Annotated[str, Field(min_length=3)] | None = None
|
|
139
140
|
trait_ids: list[str] | None = None
|
|
140
141
|
|
|
141
142
|
|
|
@@ -64,6 +64,21 @@ def configure_default_client(client: "VClient") -> None:
|
|
|
64
64
|
_default_client = client
|
|
65
65
|
|
|
66
66
|
|
|
67
|
+
def clear_default_client(client: "VClient") -> None:
|
|
68
|
+
"""Clear the default client if it matches the given instance.
|
|
69
|
+
|
|
70
|
+
Called automatically by ``VClient.close()`` so that subsequent calls to
|
|
71
|
+
``default_client()`` raise a clear ``RuntimeError`` instead of returning a
|
|
72
|
+
closed HTTP session.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
client: The client instance to compare against the current default.
|
|
76
|
+
"""
|
|
77
|
+
global _default_client # noqa: PLW0603
|
|
78
|
+
if _default_client is client:
|
|
79
|
+
_default_client = None
|
|
80
|
+
|
|
81
|
+
|
|
67
82
|
def default_client() -> "VClient":
|
|
68
83
|
"""Retrieve the configured default client.
|
|
69
84
|
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/base.py
RENAMED
|
@@ -7,6 +7,7 @@ from collections.abc import AsyncIterator
|
|
|
7
7
|
from typing import TYPE_CHECKING, Any, TypeVar
|
|
8
8
|
|
|
9
9
|
import httpx
|
|
10
|
+
from loguru import logger
|
|
10
11
|
from pydantic import BaseModel, ValidationError as PydanticValidationError
|
|
11
12
|
|
|
12
13
|
from vclient.constants import (
|
|
@@ -14,6 +15,7 @@ from vclient.constants import (
|
|
|
14
15
|
HTTP_500_INTERNAL_SERVER_ERROR,
|
|
15
16
|
HTTP_600_UPPER_BOUND,
|
|
16
17
|
IDEMPOTENCY_KEY_HEADER,
|
|
18
|
+
IDEMPOTENT_HTTP_METHODS,
|
|
17
19
|
MAX_PAGE_LIMIT,
|
|
18
20
|
RATE_LIMIT_HEADER,
|
|
19
21
|
)
|
|
@@ -109,6 +111,25 @@ class BaseService:
|
|
|
109
111
|
jitter = random.uniform(0, delay * 0.25)
|
|
110
112
|
return delay + jitter
|
|
111
113
|
|
|
114
|
+
@staticmethod
|
|
115
|
+
def _is_retryable_method(method: str, headers: dict[str, str] | None) -> bool:
|
|
116
|
+
"""Check if a request method is safe to retry.
|
|
117
|
+
|
|
118
|
+
Idempotent methods (GET, PUT, DELETE) are always safe. Non-idempotent
|
|
119
|
+
methods (POST, PATCH) are only safe if an idempotency key is present.
|
|
120
|
+
|
|
121
|
+
Args:
|
|
122
|
+
method: The HTTP method (uppercase).
|
|
123
|
+
headers: The request headers, or None.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
True if the request is safe to retry.
|
|
127
|
+
"""
|
|
128
|
+
if method in IDEMPOTENT_HTTP_METHODS:
|
|
129
|
+
return True
|
|
130
|
+
|
|
131
|
+
return IDEMPOTENCY_KEY_HEADER in (headers or {})
|
|
132
|
+
|
|
112
133
|
async def _request(
|
|
113
134
|
self,
|
|
114
135
|
method: str,
|
|
@@ -118,11 +139,15 @@ class BaseService:
|
|
|
118
139
|
json: dict[str, Any] | None = None,
|
|
119
140
|
data: dict[str, Any] | None = None,
|
|
120
141
|
headers: dict[str, str] | None = None,
|
|
142
|
+
files: Any | None = None,
|
|
121
143
|
) -> httpx.Response:
|
|
122
|
-
"""Make an HTTP request with automatic retry on
|
|
144
|
+
"""Make an HTTP request with automatic retry on transient errors.
|
|
123
145
|
|
|
124
|
-
|
|
125
|
-
|
|
146
|
+
Retries on rate limits (429), server errors (5xx in retry_statuses),
|
|
147
|
+
and network errors (ConnectError, TimeoutException). Non-idempotent
|
|
148
|
+
methods (POST, PATCH) only retry on 5xx/network errors when an
|
|
149
|
+
idempotency key header is present. Rate limit retries (429) always
|
|
150
|
+
apply regardless of method.
|
|
126
151
|
|
|
127
152
|
Args:
|
|
128
153
|
method: HTTP method (GET, POST, PUT, DELETE, etc.).
|
|
@@ -131,56 +156,109 @@ class BaseService:
|
|
|
131
156
|
json: JSON body data.
|
|
132
157
|
data: Form data.
|
|
133
158
|
headers: Additional headers to include in the request.
|
|
159
|
+
files: Files to upload (passed through to httpx).
|
|
134
160
|
|
|
135
161
|
Returns:
|
|
136
162
|
The HTTP response.
|
|
137
163
|
|
|
138
164
|
Raises:
|
|
139
165
|
RateLimitError: When rate limit is exceeded and max retries are exhausted.
|
|
166
|
+
ServerError: When server error occurs and max retries are exhausted.
|
|
167
|
+
httpx.ConnectError: When connection fails and max retries are exhausted.
|
|
168
|
+
httpx.TimeoutException: When request times out and max retries are exhausted.
|
|
140
169
|
APIError: For other API error responses.
|
|
141
170
|
"""
|
|
142
171
|
config = self._client._config # noqa: SLF001
|
|
143
172
|
max_attempts = config.max_retries + 1 if config.auto_retry_rate_limit else 1
|
|
173
|
+
retry_statuses = config.retry_statuses
|
|
174
|
+
request_logger = logger.bind(method=method, url=path)
|
|
144
175
|
|
|
145
|
-
|
|
176
|
+
request_logger.debug("Send request")
|
|
177
|
+
|
|
178
|
+
last_error: RateLimitError | ServerError | None = None
|
|
146
179
|
|
|
147
180
|
for attempt in range(max_attempts):
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
181
|
+
try:
|
|
182
|
+
response = await self._http.request(
|
|
183
|
+
method=method,
|
|
184
|
+
url=path,
|
|
185
|
+
params=params,
|
|
186
|
+
json=json,
|
|
187
|
+
data=data,
|
|
188
|
+
headers=headers,
|
|
189
|
+
files=files,
|
|
190
|
+
)
|
|
191
|
+
except (httpx.ConnectError, httpx.TimeoutException) as exc:
|
|
192
|
+
if not self._is_retryable_method(method, headers) or attempt >= max_attempts - 1:
|
|
193
|
+
raise
|
|
194
|
+
|
|
195
|
+
error_type = type(exc).__name__
|
|
196
|
+
delay = self._calculate_backoff_delay(attempt, retry_after=None)
|
|
197
|
+
request_logger.bind(
|
|
198
|
+
error_type=error_type,
|
|
199
|
+
attempt=attempt + 1,
|
|
200
|
+
max_attempts=max_attempts,
|
|
201
|
+
delay=delay,
|
|
202
|
+
).warning("Retry after network error")
|
|
203
|
+
await asyncio.sleep(delay)
|
|
204
|
+
continue
|
|
156
205
|
|
|
157
206
|
try:
|
|
158
|
-
self._raise_for_status(response)
|
|
207
|
+
self._raise_for_status(response, method, path)
|
|
208
|
+
|
|
209
|
+
elapsed_ms = response.elapsed.total_seconds() * 1000
|
|
210
|
+
request_logger.bind(
|
|
211
|
+
status=response.status_code,
|
|
212
|
+
elapsed_ms=elapsed_ms,
|
|
213
|
+
).debug("Receive response")
|
|
159
214
|
return response # noqa: TRY300
|
|
160
215
|
except RateLimitError as e:
|
|
161
216
|
last_error = e
|
|
162
217
|
|
|
163
|
-
# If this was the last attempt, don't wait
|
|
164
218
|
if attempt >= max_attempts - 1:
|
|
165
219
|
break
|
|
166
220
|
|
|
167
|
-
# Calculate backoff and wait
|
|
168
221
|
delay = self._calculate_backoff_delay(attempt, e.retry_after)
|
|
222
|
+
request_logger.bind(
|
|
223
|
+
attempt=attempt + 1,
|
|
224
|
+
max_attempts=max_attempts,
|
|
225
|
+
delay=delay,
|
|
226
|
+
).warning("Retry after rate limit")
|
|
227
|
+
await asyncio.sleep(delay)
|
|
228
|
+
except ServerError as e:
|
|
229
|
+
if e.status_code not in retry_statuses or not self._is_retryable_method(
|
|
230
|
+
method, headers
|
|
231
|
+
):
|
|
232
|
+
raise
|
|
233
|
+
|
|
234
|
+
last_error = e
|
|
235
|
+
|
|
236
|
+
if attempt >= max_attempts - 1:
|
|
237
|
+
break
|
|
238
|
+
|
|
239
|
+
delay = self._calculate_backoff_delay(attempt, retry_after=None)
|
|
240
|
+
request_logger.bind(
|
|
241
|
+
status=e.status_code,
|
|
242
|
+
attempt=attempt + 1,
|
|
243
|
+
max_attempts=max_attempts,
|
|
244
|
+
delay=delay,
|
|
245
|
+
).warning("Retry after server error")
|
|
169
246
|
await asyncio.sleep(delay)
|
|
170
247
|
|
|
171
|
-
# Re-raise the last rate limit error
|
|
172
248
|
if last_error is not None:
|
|
249
|
+
request_logger.bind(attempts=max_attempts).error("Exhaust retries")
|
|
173
250
|
raise last_error
|
|
174
251
|
|
|
175
|
-
# This should never happen, but satisfies type checker
|
|
176
252
|
msg = "Unexpected state: no response or error"
|
|
177
253
|
raise RuntimeError(msg)
|
|
178
254
|
|
|
179
|
-
def _raise_for_status(self, response: httpx.Response) -> None:
|
|
255
|
+
def _raise_for_status(self, response: httpx.Response, method: str, url: str) -> None:
|
|
180
256
|
"""Raise appropriate exception for error responses.
|
|
181
257
|
|
|
182
258
|
Args:
|
|
183
259
|
response: The HTTP response to check.
|
|
260
|
+
method: The HTTP method of the request.
|
|
261
|
+
url: The URL path of the request.
|
|
184
262
|
|
|
185
263
|
Raises:
|
|
186
264
|
AuthenticationError: For 401 responses.
|
|
@@ -203,14 +281,7 @@ class BaseService:
|
|
|
203
281
|
response_data = {}
|
|
204
282
|
message = response.text or f"HTTP {status_code}"
|
|
205
283
|
|
|
206
|
-
|
|
207
|
-
400: ValidationError,
|
|
208
|
-
401: AuthenticationError,
|
|
209
|
-
403: AuthorizationError,
|
|
210
|
-
404: NotFoundError,
|
|
211
|
-
409: ConflictError,
|
|
212
|
-
429: RateLimitError,
|
|
213
|
-
}
|
|
284
|
+
error_logger = logger.bind(method=method, url=url, status=status_code)
|
|
214
285
|
|
|
215
286
|
if status_code == 429: # noqa: PLR2004
|
|
216
287
|
retry_after = self._parse_retry_after(response)
|
|
@@ -219,8 +290,25 @@ class BaseService:
|
|
|
219
290
|
message, status_code, response_data, retry_after=retry_after, remaining=remaining
|
|
220
291
|
)
|
|
221
292
|
|
|
222
|
-
if status_code
|
|
223
|
-
|
|
293
|
+
if status_code == 401: # noqa: PLR2004
|
|
294
|
+
error_logger.error("Fail authentication")
|
|
295
|
+
raise AuthenticationError(message, status_code, response_data)
|
|
296
|
+
|
|
297
|
+
if status_code == 403: # noqa: PLR2004
|
|
298
|
+
error_logger.error("Deny authorization")
|
|
299
|
+
raise AuthorizationError(message, status_code, response_data)
|
|
300
|
+
|
|
301
|
+
if status_code == 404: # noqa: PLR2004
|
|
302
|
+
error_logger.debug("Return 404")
|
|
303
|
+
raise NotFoundError(message, status_code, response_data)
|
|
304
|
+
|
|
305
|
+
if status_code == 400: # noqa: PLR2004
|
|
306
|
+
error_logger.warning("Reject with validation error")
|
|
307
|
+
raise ValidationError(message, status_code, response_data)
|
|
308
|
+
|
|
309
|
+
if status_code == 409: # noqa: PLR2004
|
|
310
|
+
error_logger.warning("Return 409 conflict")
|
|
311
|
+
raise ConflictError(message, status_code, response_data)
|
|
224
312
|
|
|
225
313
|
if HTTP_500_INTERNAL_SERVER_ERROR <= status_code < HTTP_600_UPPER_BOUND:
|
|
226
314
|
raise ServerError(message, status_code, response_data)
|
|
@@ -480,40 +568,18 @@ class BaseService:
|
|
|
480
568
|
The HTTP response.
|
|
481
569
|
|
|
482
570
|
Raises:
|
|
571
|
+
ServerError: When server error occurs and max retries are exhausted.
|
|
483
572
|
RateLimitError: When rate limit is exceeded and max retries are exhausted.
|
|
484
573
|
APIError: For other API error responses.
|
|
485
574
|
"""
|
|
486
|
-
config = self._client._config # noqa: SLF001
|
|
487
|
-
max_attempts = config.max_retries + 1 if config.auto_retry_rate_limit else 1
|
|
488
|
-
|
|
489
|
-
last_error: RateLimitError | None = None
|
|
490
|
-
headers = self._build_idempotency_headers(idempotency_key)
|
|
491
575
|
filename, content, content_type = file
|
|
492
576
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
try:
|
|
501
|
-
self._raise_for_status(response)
|
|
502
|
-
return response # noqa: TRY300
|
|
503
|
-
except RateLimitError as e:
|
|
504
|
-
last_error = e
|
|
505
|
-
|
|
506
|
-
if attempt >= max_attempts - 1:
|
|
507
|
-
break
|
|
508
|
-
|
|
509
|
-
delay = self._calculate_backoff_delay(attempt, e.retry_after)
|
|
510
|
-
await asyncio.sleep(delay)
|
|
511
|
-
|
|
512
|
-
if last_error is not None:
|
|
513
|
-
raise last_error
|
|
514
|
-
|
|
515
|
-
msg = "Unexpected state: no response or error"
|
|
516
|
-
raise RuntimeError(msg)
|
|
577
|
+
return await self._request(
|
|
578
|
+
"POST",
|
|
579
|
+
path,
|
|
580
|
+
files={"file": (filename, content, content_type)},
|
|
581
|
+
headers=self._build_idempotency_headers(idempotency_key),
|
|
582
|
+
)
|
|
517
583
|
|
|
518
584
|
# -------------------------------------------------------------------------
|
|
519
585
|
# Pagination Methods
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/chapters.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/developers.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/global_admin.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/pagination.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/models/system.py
RENAMED
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/campaigns.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/characters.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/companies.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/developers.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/dicerolls.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/dictionary.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/global_admin.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/options.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/system.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/services/users.py
RENAMED
|
File without changes
|
{valentina_python_client-1.2.1 → valentina_python_client-1.3.1}/src/vclient/validate_constants.py
RENAMED
|
File without changes
|