valentina-python-client 2.9.0__tar.gz → 3.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.

Potentially problematic release.


This version of valentina-python-client might be problematic. Click here for more details.

Files changed (84) hide show
  1. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/PKG-INFO +3 -3
  2. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/README.md +1 -1
  3. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/pyproject.toml +8 -6
  4. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/__init__.py +1 -1
  5. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/client.py +5 -5
  6. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/base.py +19 -19
  7. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/campaign_book_chapters.py +2 -2
  8. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/testing/_client.py +5 -5
  9. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/client.py +5 -5
  10. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/books.py +4 -0
  11. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/chapters.py +11 -0
  12. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/characters.py +4 -0
  13. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/base.py +19 -19
  14. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/campaign_book_chapters.py +2 -2
  15. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/testing/_client.py +5 -5
  16. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/testing/_router.py +17 -17
  17. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/LICENSE +0 -0
  18. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_codegen.py +0 -0
  19. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/__init__.py +0 -0
  20. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/registry.py +0 -0
  21. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/__init__.py +0 -0
  22. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/_audit_params.py +0 -0
  23. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/campaign_books.py +0 -0
  24. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/campaigns.py +0 -0
  25. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/character_autogen.py +0 -0
  26. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/character_blueprint.py +0 -0
  27. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/character_traits.py +0 -0
  28. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/characters.py +0 -0
  29. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/companies.py +0 -0
  30. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/developers.py +0 -0
  31. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/dicerolls.py +0 -0
  32. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/dictionary.py +0 -0
  33. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/global_admin.py +0 -0
  34. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/identity.py +0 -0
  35. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/options.py +0 -0
  36. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/system.py +0 -0
  37. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/user_lookup.py +0 -0
  38. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/services/users.py +0 -0
  39. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/_sync/testing/__init__.py +0 -0
  40. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/config.py +0 -0
  41. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/constants.py +0 -0
  42. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/endpoints.py +0 -0
  43. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/exceptions.py +0 -0
  44. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/__init__.py +0 -0
  45. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/audit_logs.py +0 -0
  46. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/campaigns.py +0 -0
  47. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/character_autogen.py +0 -0
  48. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/character_blueprint.py +0 -0
  49. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/character_trait.py +0 -0
  50. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/companies.py +0 -0
  51. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/developers.py +0 -0
  52. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/diceroll.py +0 -0
  53. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/dictionary.py +0 -0
  54. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/full_sheet.py +0 -0
  55. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/global_admin.py +0 -0
  56. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/pagination.py +0 -0
  57. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/shared.py +0 -0
  58. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/system.py +0 -0
  59. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/user_lookup.py +0 -0
  60. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/models/users.py +0 -0
  61. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/py.typed +0 -0
  62. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/registry.py +0 -0
  63. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/__init__.py +0 -0
  64. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/_audit_params.py +0 -0
  65. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/campaign_books.py +0 -0
  66. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/campaigns.py +0 -0
  67. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/character_autogen.py +0 -0
  68. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/character_blueprint.py +0 -0
  69. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/character_traits.py +0 -0
  70. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/characters.py +0 -0
  71. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/companies.py +0 -0
  72. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/developers.py +0 -0
  73. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/dicerolls.py +0 -0
  74. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/dictionary.py +0 -0
  75. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/global_admin.py +0 -0
  76. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/identity.py +0 -0
  77. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/options.py +0 -0
  78. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/system.py +0 -0
  79. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/user_lookup.py +0 -0
  80. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/services/users.py +0 -0
  81. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/testing/__init__.py +0 -0
  82. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/testing/_factories.py +0 -0
  83. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/src/vclient/testing/_routes.py +0 -0
  84. {valentina_python_client-2.9.0 → valentina_python_client-3.1.0}/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: 2.9.0
3
+ Version: 3.1.0
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>
@@ -16,7 +16,7 @@ License: MIT License
16
16
  Classifier: Programming Language :: Python :: 3.13
17
17
  Classifier: Programming Language :: Python :: 3.14
18
18
  Requires-Dist: anyio>=4.13.0,<5.0.0
19
- Requires-Dist: httpx>=0.28.1
19
+ Requires-Dist: httpx2>=2.4.0
20
20
  Requires-Dist: loguru>=0.7.3
21
21
  Requires-Dist: polyfactory>=3.3.0,<4.0.0
22
22
  Requires-Dist: pydantic[email]>=2.13.4,<3.0.0
@@ -33,7 +33,7 @@ Async and sync Python client library for accessing the Valentina Noir API.
33
33
 
34
34
  ## Features
35
35
 
36
- - **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on httpx
36
+ - **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on httpx2
37
37
  - **Type-safe** - Full type hints with Pydantic models for request/response validation
38
38
  - **Convenient factory pattern** - Create a client once, access services from anywhere
39
39
  - **Automatic pagination** - Stream through large datasets with `iter_all()` or fetch everything with `list_all()`
@@ -4,7 +4,7 @@ Async and sync Python client library for accessing the Valentina Noir API.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on httpx
7
+ - **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on httpx2
8
8
  - **Type-safe** - Full type hints with Pydantic models for request/response validation
9
9
  - **Convenient factory pattern** - Create a client once, access services from anywhere
10
10
  - **Automatic pagination** - Stream through large datasets with `iter_all()` or fetch everything with `list_all()`
@@ -6,7 +6,7 @@
6
6
  ]
7
7
  dependencies = [
8
8
  "anyio>=4.13.0, <5.0.0",
9
- "httpx>=0.28.1",
9
+ "httpx2>=2.4.0",
10
10
  "loguru>=0.7.3",
11
11
  "polyfactory>=3.3.0, <4.0.0",
12
12
  "pydantic[email]>=2.13.4, <3.0.0",
@@ -16,7 +16,7 @@
16
16
  name = "valentina-python-client"
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.13"
19
- version = "2.9.0"
19
+ version = "3.1.0"
20
20
 
21
21
  [project.optional-dependencies]
22
22
  testing = ["polyfactory>=3.3.0"]
@@ -37,25 +37,27 @@
37
37
  "commitizen>=4.16.3",
38
38
  "coverage>=7.14.1",
39
39
  "duty>=1.9.0",
40
+ "httpx>=0.28.1",
40
41
  "prek>=0.4.4",
41
42
  "pytest-anyio>=0.0.0",
42
43
  "pytest-clarity>=1.0.1",
43
44
  "pytest-cov>=7.1.0",
44
45
  "pytest-devtools>=1.2.0",
46
+ "pytest-httpx2>=1.0.0",
45
47
  "pytest-mock>=3.15.1",
46
48
  "pytest-repeat>=0.9.4",
47
49
  "pytest-sugar>=1.1.1",
48
50
  "pytest-xdist>=3.8.0",
49
- "pytest>=9.0.3",
51
+ "pytest>=9.1.0",
50
52
  "respx>=0.23.1",
51
- "ruff>=0.15.16",
53
+ "ruff>=0.15.17",
52
54
  "shellcheck-py>=0.11.0.1",
53
- "ty>=0.0.44",
55
+ "ty>=0.0.49",
54
56
  "typos>=1.47.2",
55
57
  "vulture>=2.16",
56
58
  "yamllint>=1.38.0",
57
59
  ]
58
- docs = ["zensical>=0.0.44"]
60
+ docs = ["zensical>=0.0.45"]
59
61
 
60
62
  [tool.commitizen]
61
63
  bump_message = "bump(release): v$current_version → v$new_version"
@@ -116,4 +116,4 @@ __all__ = (
116
116
  "users_service",
117
117
  )
118
118
 
119
- __version__ = "2.9.0"
119
+ __version__ = "3.1.0"
@@ -6,7 +6,7 @@ import platform
6
6
  from types import TracebackType
7
7
  from typing import TYPE_CHECKING, Self
8
8
 
9
- import httpx
9
+ import httpx2
10
10
  from loguru import logger
11
11
 
12
12
  from vclient.config import _APIConfig
@@ -144,7 +144,7 @@ class SyncVClient:
144
144
  default_company_id=resolved_company_id,
145
145
  headers=headers or {},
146
146
  )
147
- self._http: httpx.Client = self._create_http_client()
147
+ self._http: httpx2.Client = self._create_http_client()
148
148
  self._companies: SyncCompaniesService | None = None
149
149
  self._developer: SyncDeveloperService | None = None
150
150
  self._global_admin: SyncGlobalAdminService | None = None
@@ -160,7 +160,7 @@ class SyncVClient:
160
160
  max_retries=self._config.max_retries,
161
161
  ).info("Initialize SyncVClient")
162
162
 
163
- def _create_http_client(self) -> httpx.Client:
163
+ def _create_http_client(self) -> httpx2.Client:
164
164
  """Create and configure the HTTP client."""
165
165
  from vclient import __version__
166
166
 
@@ -168,10 +168,10 @@ class SyncVClient:
168
168
  headers = {"Accept": "application/json", "User-Agent": user_agent, **self._config.headers}
169
169
  if self._config.api_key:
170
170
  headers[API_KEY_HEADER] = self._config.api_key
171
- return httpx.Client(
171
+ return httpx2.Client(
172
172
  base_url=self._config.base_url,
173
173
  headers=headers,
174
- timeout=httpx.Timeout(self._config.timeout),
174
+ timeout=httpx2.Timeout(self._config.timeout),
175
175
  )
176
176
 
177
177
  def __enter__(self) -> Self:
@@ -7,7 +7,7 @@ import uuid
7
7
  from collections.abc import Iterator
8
8
  from typing import TYPE_CHECKING, Any, TypeVar
9
9
 
10
- import httpx
10
+ import httpx2
11
11
  from loguru import logger
12
12
  from pydantic import BaseModel, ValidationError as PydanticValidationError
13
13
 
@@ -57,7 +57,7 @@ class SyncBaseService:
57
57
  self._on_behalf_of: str | None = None
58
58
 
59
59
  @property
60
- def _http(self) -> httpx.Client:
60
+ def _http(self) -> httpx2.Client:
61
61
  """Get the HTTP client from the parent SyncVClient."""
62
62
  return self._client._http
63
63
 
@@ -137,7 +137,7 @@ class SyncBaseService:
137
137
  data: dict[str, Any] | None = None,
138
138
  headers: dict[str, str] | None = None,
139
139
  files: Any | None = None,
140
- ) -> httpx.Response:
140
+ ) -> httpx2.Response:
141
141
  """Make an HTTP request with automatic retry on transient errors.
142
142
 
143
143
  Retries on rate limits (429), server errors (5xx in retry_statuses),
@@ -153,7 +153,7 @@ class SyncBaseService:
153
153
  json: JSON body data.
154
154
  data: Form data.
155
155
  headers: Additional headers to include in the request.
156
- files: Files to upload (passed through to httpx).
156
+ files: Files to upload (passed through to httpx2).
157
157
 
158
158
  Returns:
159
159
  The HTTP response.
@@ -161,8 +161,8 @@ class SyncBaseService:
161
161
  Raises:
162
162
  RateLimitError: When rate limit is exceeded and max retries are exhausted.
163
163
  ServerError: When server error occurs and max retries are exhausted.
164
- httpx.ConnectError: When connection fails and max retries are exhausted.
165
- httpx.TimeoutException: When request times out and max retries are exhausted.
164
+ httpx2.ConnectError: When connection fails and max retries are exhausted.
165
+ httpx2.TimeoutException: When request times out and max retries are exhausted.
166
166
  APIError: For other API error responses.
167
167
  """
168
168
  headers = self._merge_on_behalf_of_header(headers)
@@ -183,7 +183,7 @@ class SyncBaseService:
183
183
  headers=headers,
184
184
  files=files,
185
185
  )
186
- except (httpx.ConnectError, httpx.TimeoutException) as exc:
186
+ except (httpx2.ConnectError, httpx2.TimeoutException) as exc:
187
187
  if not self._is_retryable_method(method, headers) or attempt >= max_attempts - 1:
188
188
  raise
189
189
  error_type = type(exc).__name__
@@ -232,7 +232,7 @@ class SyncBaseService:
232
232
  raise RuntimeError(msg)
233
233
 
234
234
  @staticmethod
235
- def _log_success_response(response: httpx.Response, request_logger: Any) -> None:
235
+ def _log_success_response(response: httpx2.Response, request_logger: Any) -> None:
236
236
  """Log a successful HTTP response with elapsed time and optional request_id.
237
237
 
238
238
  Args:
@@ -248,7 +248,7 @@ class SyncBaseService:
248
248
 
249
249
  @staticmethod
250
250
  def _inject_request_id_fallback(
251
- response_data: dict[str, Any], response: httpx.Response
251
+ response_data: dict[str, Any], response: httpx2.Response
252
252
  ) -> None:
253
253
  """Inject request_id from X-Request-Id header when the response body omits it.
254
254
 
@@ -262,7 +262,7 @@ class SyncBaseService:
262
262
  response_data["request_id"] = header_id
263
263
 
264
264
  def _raise_for_status(
265
- self, response: httpx.Response, method: str, url: str, params: dict[str, Any] | None = None
265
+ self, response: httpx2.Response, method: str, url: str, params: dict[str, Any] | None = None
266
266
  ) -> None:
267
267
  """Raise appropriate exception for error responses.
268
268
 
@@ -356,7 +356,7 @@ class SyncBaseService:
356
356
  return None
357
357
 
358
358
  @staticmethod
359
- def _parse_retry_after(response: httpx.Response) -> int | None:
359
+ def _parse_retry_after(response: httpx2.Response) -> int | None:
360
360
  """Parse the retry time from response headers.
361
361
 
362
362
  First checks the RateLimit header for the "t" parameter (seconds until next token),
@@ -382,7 +382,7 @@ class SyncBaseService:
382
382
  return None
383
383
 
384
384
  @staticmethod
385
- def _parse_remaining_tokens(response: httpx.Response) -> int | None:
385
+ def _parse_remaining_tokens(response: httpx2.Response) -> int | None:
386
386
  """Parse the remaining tokens from the RateLimit header.
387
387
 
388
388
  Args:
@@ -402,7 +402,7 @@ class SyncBaseService:
402
402
  *,
403
403
  params: dict[str, Any] | None = None,
404
404
  headers: dict[str, str] | None = None,
405
- ) -> httpx.Response:
405
+ ) -> httpx2.Response:
406
406
  """Make a GET request.
407
407
 
408
408
  Args:
@@ -466,7 +466,7 @@ class SyncBaseService:
466
466
  data: dict[str, Any] | None = None,
467
467
  params: dict[str, Any] | None = None,
468
468
  idempotency_key: str | None = None,
469
- ) -> httpx.Response:
469
+ ) -> httpx2.Response:
470
470
  """Make a POST request.
471
471
 
472
472
  Args:
@@ -498,7 +498,7 @@ class SyncBaseService:
498
498
  data: dict[str, Any] | None = None,
499
499
  params: dict[str, Any] | None = None,
500
500
  idempotency_key: str | None = None,
501
- ) -> httpx.Response:
501
+ ) -> httpx2.Response:
502
502
  """Make a PUT request.
503
503
 
504
504
  Args:
@@ -530,7 +530,7 @@ class SyncBaseService:
530
530
  data: dict[str, Any] | None = None,
531
531
  params: dict[str, Any] | None = None,
532
532
  idempotency_key: str | None = None,
533
- ) -> httpx.Response:
533
+ ) -> httpx2.Response:
534
534
  """Make a PATCH request.
535
535
 
536
536
  Args:
@@ -554,7 +554,7 @@ class SyncBaseService:
554
554
  headers=self._build_idempotency_headers(idempotency_key),
555
555
  )
556
556
 
557
- def _delete(self, path: str, *, params: dict[str, Any] | None = None) -> httpx.Response:
557
+ def _delete(self, path: str, *, params: dict[str, Any] | None = None) -> httpx2.Response:
558
558
  """Make a DELETE request.
559
559
 
560
560
  Args:
@@ -568,7 +568,7 @@ class SyncBaseService:
568
568
 
569
569
  def _post_file(
570
570
  self, path: str, *, file: tuple[str, bytes, str], idempotency_key: str | None = None
571
- ) -> httpx.Response:
571
+ ) -> httpx2.Response:
572
572
  """Make a POST request with a file upload (multipart/form-data).
573
573
 
574
574
  Args:
@@ -594,7 +594,7 @@ class SyncBaseService:
594
594
 
595
595
  def _put_file(
596
596
  self, path: str, *, file: tuple[str, bytes, str], idempotency_key: str | None = None
597
- ) -> httpx.Response:
597
+ ) -> httpx2.Response:
598
598
  """Make a PUT request with a file upload (multipart/form-data).
599
599
 
600
600
  Mirrors ``_post_file``: an idempotency key is sent only when supplied.
@@ -114,7 +114,7 @@ class SyncChaptersService(SyncBaseService):
114
114
  Args:
115
115
  request: A ChapterCreate model, OR pass fields as keyword arguments.
116
116
  **kwargs: Fields for ChapterCreate if request is not provided.
117
- Accepts: name (str, required), description (str | None).
117
+ Accepts: name (str, required), description (str | None), character_ids (list[str] | None).
118
118
  """
119
119
  body = request if request is not None else ChapterCreate(**kwargs)
120
120
  response = self._post(
@@ -132,7 +132,7 @@ class SyncChaptersService(SyncBaseService):
132
132
  chapter_id: The ID of the chapter to update.
133
133
  request: A ChapterUpdate model, OR pass fields as keyword arguments.
134
134
  **kwargs: Fields for ChapterUpdate if request is not provided.
135
- Accepts: name (str | None), description (str | None).
135
+ Accepts: name (str | None), description (str | None), character_ids (list[str] | None).
136
136
  """
137
137
  body = request if request is not None else ChapterUpdate(**kwargs)
138
138
  response = self._patch(
@@ -1,7 +1,7 @@
1
1
  # AUTO-GENERATED — do not edit. Run 'uv run duty generate_sync' to regenerate.
2
2
  """Fake async API client for testing downstream applications.
3
3
 
4
- FakeSyncVClient is a drop-in replacement for SyncVClient that uses httpx.MockTransport
4
+ FakeSyncVClient is a drop-in replacement for SyncVClient that uses httpx2.MockTransport
5
5
  instead of real HTTP. All real service classes work unmodified.
6
6
  """
7
7
 
@@ -10,7 +10,7 @@ from __future__ import annotations
10
10
  import secrets
11
11
  from typing import TYPE_CHECKING, Any
12
12
 
13
- import httpx
13
+ import httpx2
14
14
  from pydantic import BaseModel
15
15
 
16
16
  from vclient._sync.client import SyncVClient
@@ -65,10 +65,10 @@ class SyncFakeVClient(SyncVClient):
65
65
  **kwargs,
66
66
  )
67
67
 
68
- def _create_http_client(self) -> httpx.Client:
68
+ def _create_http_client(self) -> httpx2.Client:
69
69
  """Create an HTTP client backed by the fake router."""
70
- return httpx.Client(
71
- transport=httpx.MockTransport(self._router.handle),
70
+ return httpx2.Client(
71
+ transport=httpx2.MockTransport(self._router.handle),
72
72
  base_url="https://fake.valentina-api.test",
73
73
  )
74
74
 
@@ -5,7 +5,7 @@ import platform
5
5
  from types import TracebackType
6
6
  from typing import TYPE_CHECKING, Self
7
7
 
8
- import httpx
8
+ import httpx2
9
9
  from loguru import logger
10
10
 
11
11
  from vclient.config import _APIConfig
@@ -147,7 +147,7 @@ class VClient:
147
147
  headers=headers or {},
148
148
  )
149
149
 
150
- self._http: httpx.AsyncClient = self._create_http_client()
150
+ self._http: httpx2.AsyncClient = self._create_http_client()
151
151
  self._companies: CompaniesService | None = None
152
152
  self._developer: DeveloperService | None = None
153
153
  self._global_admin: GlobalAdminService | None = None
@@ -165,7 +165,7 @@ class VClient:
165
165
  max_retries=self._config.max_retries,
166
166
  ).info("Initialize VClient")
167
167
 
168
- def _create_http_client(self) -> httpx.AsyncClient:
168
+ def _create_http_client(self) -> httpx2.AsyncClient:
169
169
  """Create and configure the HTTP client."""
170
170
  from vclient import __version__
171
171
 
@@ -180,10 +180,10 @@ class VClient:
180
180
  if self._config.api_key:
181
181
  headers[API_KEY_HEADER] = self._config.api_key
182
182
 
183
- return httpx.AsyncClient(
183
+ return httpx2.AsyncClient(
184
184
  base_url=self._config.base_url,
185
185
  headers=headers,
186
- timeout=httpx.Timeout(self._config.timeout),
186
+ timeout=httpx2.Timeout(self._config.timeout),
187
187
  )
188
188
 
189
189
  async def __aenter__(self) -> Self:
@@ -20,6 +20,10 @@ class CampaignBook(BaseModel):
20
20
  name: str = Field(..., description="Book name (3-50 characters).")
21
21
  description: str | None = Field(default=None, description="Book description.")
22
22
  asset_ids: list[str] = Field(default_factory=list, description="List of associated asset IDs.")
23
+ character_ids: list[str] = Field(
24
+ default_factory=list,
25
+ description="Read-only union of character IDs across the book's chapters.",
26
+ )
23
27
  number: int = Field(..., description="Book number within the campaign.")
24
28
  campaign_id: str = Field(..., description="ID of the parent campaign.")
25
29
  num_chapters: int = Field(default=0, description="Number of active chapters in the book.")
@@ -21,6 +21,9 @@ class CampaignChapter(BaseModel):
21
21
  name: str = Field(..., description="Chapter name (3-50 characters).")
22
22
  description: str | None = Field(default=None, description="Chapter description.")
23
23
  asset_ids: list[str] = Field(default_factory=list, description="List of associated asset IDs.")
24
+ character_ids: list[str] = Field(
25
+ default_factory=list, description="List of associated character IDs."
26
+ )
24
27
  number: int = Field(..., description="Chapter number within the book.")
25
28
  book_id: str = Field(..., description="ID of the parent book.")
26
29
  num_notes: int = Field(default=0, description="Number of active notes on the chapter.")
@@ -48,6 +51,10 @@ class ChapterCreate(BaseModel):
48
51
 
49
52
  name: str = Field(..., description="Chapter name (3-50 characters).")
50
53
  description: str | None = Field(default=None, description="Chapter description.")
54
+ character_ids: list[str] | None = Field(
55
+ default=None,
56
+ description="Character IDs to associate. Each must be an active character in the same campaign.",
57
+ )
51
58
 
52
59
 
53
60
  class ChapterUpdate(BaseModel):
@@ -55,6 +62,10 @@ class ChapterUpdate(BaseModel):
55
62
 
56
63
  name: str | None = Field(default=None, description="Chapter name (3-50 characters).")
57
64
  description: str | None = Field(default=None, description="Chapter description.")
65
+ character_ids: list[str] | None = Field(
66
+ default=None,
67
+ description="Replacement character ID list. Omit to leave unchanged; send [] to clear.",
68
+ )
58
69
 
59
70
 
60
71
  class _ChapterRenumber(BaseModel):
@@ -204,6 +204,10 @@ class Character(BaseModel):
204
204
  character_trait_ids: list[str] = Field(
205
205
  default_factory=list, description="List of character trait IDs."
206
206
  )
207
+ chapter_ids: list[str] = Field(
208
+ default_factory=list,
209
+ description="Read-only IDs of non-archived chapters this character is associated with.",
210
+ )
207
211
  specialties: list[CharacterSpecialty] = Field(
208
212
  default_factory=list, description="List of character specialties."
209
213
  )
@@ -6,7 +6,7 @@ import uuid
6
6
  from collections.abc import AsyncIterator
7
7
  from typing import TYPE_CHECKING, Any, TypeVar
8
8
 
9
- import httpx
9
+ import httpx2
10
10
  from loguru import logger
11
11
  from pydantic import BaseModel, ValidationError as PydanticValidationError
12
12
 
@@ -57,7 +57,7 @@ class BaseService:
57
57
  self._on_behalf_of: str | None = None
58
58
 
59
59
  @property
60
- def _http(self) -> httpx.AsyncClient:
60
+ def _http(self) -> httpx2.AsyncClient:
61
61
  """Get the HTTP client from the parent VClient."""
62
62
  return self._client._http # noqa: SLF001
63
63
 
@@ -144,7 +144,7 @@ class BaseService:
144
144
  data: dict[str, Any] | None = None,
145
145
  headers: dict[str, str] | None = None,
146
146
  files: Any | None = None,
147
- ) -> httpx.Response:
147
+ ) -> httpx2.Response:
148
148
  """Make an HTTP request with automatic retry on transient errors.
149
149
 
150
150
  Retries on rate limits (429), server errors (5xx in retry_statuses),
@@ -160,7 +160,7 @@ class BaseService:
160
160
  json: JSON body data.
161
161
  data: Form data.
162
162
  headers: Additional headers to include in the request.
163
- files: Files to upload (passed through to httpx).
163
+ files: Files to upload (passed through to httpx2).
164
164
 
165
165
  Returns:
166
166
  The HTTP response.
@@ -168,8 +168,8 @@ class BaseService:
168
168
  Raises:
169
169
  RateLimitError: When rate limit is exceeded and max retries are exhausted.
170
170
  ServerError: When server error occurs and max retries are exhausted.
171
- httpx.ConnectError: When connection fails and max retries are exhausted.
172
- httpx.TimeoutException: When request times out and max retries are exhausted.
171
+ httpx2.ConnectError: When connection fails and max retries are exhausted.
172
+ httpx2.TimeoutException: When request times out and max retries are exhausted.
173
173
  APIError: For other API error responses.
174
174
  """
175
175
  headers = self._merge_on_behalf_of_header(headers)
@@ -198,7 +198,7 @@ class BaseService:
198
198
  headers=headers,
199
199
  files=files,
200
200
  )
201
- except (httpx.ConnectError, httpx.TimeoutException) as exc:
201
+ except (httpx2.ConnectError, httpx2.TimeoutException) as exc:
202
202
  if not self._is_retryable_method(method, headers) or attempt >= max_attempts - 1:
203
203
  raise
204
204
 
@@ -258,7 +258,7 @@ class BaseService:
258
258
  raise RuntimeError(msg)
259
259
 
260
260
  @staticmethod
261
- def _log_success_response(response: httpx.Response, request_logger: Any) -> None:
261
+ def _log_success_response(response: httpx2.Response, request_logger: Any) -> None:
262
262
  """Log a successful HTTP response with elapsed time and optional request_id.
263
263
 
264
264
  Args:
@@ -277,7 +277,7 @@ class BaseService:
277
277
 
278
278
  @staticmethod
279
279
  def _inject_request_id_fallback(
280
- response_data: dict[str, Any], response: httpx.Response
280
+ response_data: dict[str, Any], response: httpx2.Response
281
281
  ) -> None:
282
282
  """Inject request_id from X-Request-Id header when the response body omits it.
283
283
 
@@ -292,7 +292,7 @@ class BaseService:
292
292
 
293
293
  def _raise_for_status( # noqa: C901
294
294
  self,
295
- response: httpx.Response,
295
+ response: httpx2.Response,
296
296
  method: str,
297
297
  url: str,
298
298
  params: dict[str, Any] | None = None,
@@ -407,7 +407,7 @@ class BaseService:
407
407
  return None
408
408
 
409
409
  @staticmethod
410
- def _parse_retry_after(response: httpx.Response) -> int | None:
410
+ def _parse_retry_after(response: httpx2.Response) -> int | None:
411
411
  """Parse the retry time from response headers.
412
412
 
413
413
  First checks the RateLimit header for the "t" parameter (seconds until next token),
@@ -437,7 +437,7 @@ class BaseService:
437
437
  return None
438
438
 
439
439
  @staticmethod
440
- def _parse_remaining_tokens(response: httpx.Response) -> int | None:
440
+ def _parse_remaining_tokens(response: httpx2.Response) -> int | None:
441
441
  """Parse the remaining tokens from the RateLimit header.
442
442
 
443
443
  Args:
@@ -458,7 +458,7 @@ class BaseService:
458
458
  *,
459
459
  params: dict[str, Any] | None = None,
460
460
  headers: dict[str, str] | None = None,
461
- ) -> httpx.Response:
461
+ ) -> httpx2.Response:
462
462
  """Make a GET request.
463
463
 
464
464
  Args:
@@ -528,7 +528,7 @@ class BaseService:
528
528
  data: dict[str, Any] | None = None,
529
529
  params: dict[str, Any] | None = None,
530
530
  idempotency_key: str | None = None,
531
- ) -> httpx.Response:
531
+ ) -> httpx2.Response:
532
532
  """Make a POST request.
533
533
 
534
534
  Args:
@@ -561,7 +561,7 @@ class BaseService:
561
561
  data: dict[str, Any] | None = None,
562
562
  params: dict[str, Any] | None = None,
563
563
  idempotency_key: str | None = None,
564
- ) -> httpx.Response:
564
+ ) -> httpx2.Response:
565
565
  """Make a PUT request.
566
566
 
567
567
  Args:
@@ -594,7 +594,7 @@ class BaseService:
594
594
  data: dict[str, Any] | None = None,
595
595
  params: dict[str, Any] | None = None,
596
596
  idempotency_key: str | None = None,
597
- ) -> httpx.Response:
597
+ ) -> httpx2.Response:
598
598
  """Make a PATCH request.
599
599
 
600
600
  Args:
@@ -624,7 +624,7 @@ class BaseService:
624
624
  path: str,
625
625
  *,
626
626
  params: dict[str, Any] | None = None,
627
- ) -> httpx.Response:
627
+ ) -> httpx2.Response:
628
628
  """Make a DELETE request.
629
629
 
630
630
  Args:
@@ -642,7 +642,7 @@ class BaseService:
642
642
  *,
643
643
  file: tuple[str, bytes, str],
644
644
  idempotency_key: str | None = None,
645
- ) -> httpx.Response:
645
+ ) -> httpx2.Response:
646
646
  """Make a POST request with a file upload (multipart/form-data).
647
647
 
648
648
  Args:
@@ -673,7 +673,7 @@ class BaseService:
673
673
  *,
674
674
  file: tuple[str, bytes, str],
675
675
  idempotency_key: str | None = None,
676
- ) -> httpx.Response:
676
+ ) -> httpx2.Response:
677
677
  """Make a PUT request with a file upload (multipart/form-data).
678
678
 
679
679
  Mirrors ``_post_file``: an idempotency key is sent only when supplied.
@@ -132,7 +132,7 @@ class ChaptersService(BaseService):
132
132
  Args:
133
133
  request: A ChapterCreate model, OR pass fields as keyword arguments.
134
134
  **kwargs: Fields for ChapterCreate if request is not provided.
135
- Accepts: name (str, required), description (str | None).
135
+ Accepts: name (str, required), description (str | None), character_ids (list[str] | None).
136
136
  """
137
137
  body = request if request is not None else ChapterCreate(**kwargs)
138
138
  response = await self._post(
@@ -153,7 +153,7 @@ class ChaptersService(BaseService):
153
153
  chapter_id: The ID of the chapter to update.
154
154
  request: A ChapterUpdate model, OR pass fields as keyword arguments.
155
155
  **kwargs: Fields for ChapterUpdate if request is not provided.
156
- Accepts: name (str | None), description (str | None).
156
+ Accepts: name (str | None), description (str | None), character_ids (list[str] | None).
157
157
  """
158
158
  body = request if request is not None else ChapterUpdate(**kwargs)
159
159
  response = await self._patch(
@@ -1,6 +1,6 @@
1
1
  """Fake async API client for testing downstream applications.
2
2
 
3
- FakeVClient is a drop-in replacement for VClient that uses httpx.MockTransport
3
+ FakeVClient is a drop-in replacement for VClient that uses httpx2.MockTransport
4
4
  instead of real HTTP. All real service classes work unmodified.
5
5
  """
6
6
 
@@ -9,7 +9,7 @@ from __future__ import annotations
9
9
  import secrets
10
10
  from typing import TYPE_CHECKING, Any
11
11
 
12
- import httpx
12
+ import httpx2
13
13
  from pydantic import BaseModel
14
14
 
15
15
  from vclient.client import VClient
@@ -64,10 +64,10 @@ class FakeVClient(VClient):
64
64
  **kwargs,
65
65
  )
66
66
 
67
- def _create_http_client(self) -> httpx.AsyncClient:
67
+ def _create_http_client(self) -> httpx2.AsyncClient:
68
68
  """Create an HTTP client backed by the fake router."""
69
- return httpx.AsyncClient(
70
- transport=httpx.MockTransport(self._router.handle),
69
+ return httpx2.AsyncClient(
70
+ transport=httpx2.MockTransport(self._router.handle),
71
71
  base_url="https://fake.valentina-api.test",
72
72
  )
73
73
 
@@ -7,7 +7,7 @@ import re
7
7
  import secrets
8
8
  from typing import TYPE_CHECKING, Any
9
9
 
10
- import httpx
10
+ import httpx2
11
11
 
12
12
  if TYPE_CHECKING:
13
13
  from polyfactory.factories.pydantic_factory import ModelFactory
@@ -217,22 +217,22 @@ class _Route:
217
217
 
218
218
  return True
219
219
 
220
- def respond(self) -> httpx.Response: # noqa: PLR0911
221
- """Generate an httpx.Response for this route."""
220
+ def respond(self) -> httpx2.Response: # noqa: PLR0911
221
+ """Generate an httpx2.Response for this route."""
222
222
  if self.override_json is not None:
223
- return httpx.Response(
223
+ return httpx2.Response(
224
224
  status_code=self.override_status or 200,
225
225
  json=self.override_json,
226
226
  )
227
227
 
228
228
  if self.style == NO_CONTENT:
229
- return httpx.Response(status_code=204)
229
+ return httpx2.Response(status_code=204)
230
230
 
231
231
  if self.style == RAW_JSON:
232
- return httpx.Response(status_code=200, json={})
232
+ return httpx2.Response(status_code=200, json={})
233
233
 
234
234
  if self.style == BYTES:
235
- return httpx.Response(
235
+ return httpx2.Response(
236
236
  status_code=200,
237
237
  content=b"PK\x03\x04fake-log-archive",
238
238
  headers={"Content-Disposition": 'attachment; filename="vapi-logs-fake.zip"'},
@@ -246,7 +246,7 @@ class _Route:
246
246
  instance_data = instance.model_dump(mode="json")
247
247
 
248
248
  if self.style == LIST:
249
- return httpx.Response(status_code=200, json=[instance_data])
249
+ return httpx2.Response(status_code=200, json=[instance_data])
250
250
 
251
251
  if self.style == PAGINATED:
252
252
  body = {
@@ -255,9 +255,9 @@ class _Route:
255
255
  "limit": 10,
256
256
  "offset": 0,
257
257
  }
258
- return httpx.Response(status_code=200, json=body)
258
+ return httpx2.Response(status_code=200, json=body)
259
259
 
260
- return httpx.Response(status_code=200, json=instance_data)
260
+ return httpx2.Response(status_code=200, json=instance_data)
261
261
 
262
262
 
263
263
  class _FakeRouter:
@@ -311,17 +311,17 @@ class _FakeRouter:
311
311
  )
312
312
  )
313
313
 
314
- def handle(self, request: httpx.Request) -> httpx.Response:
314
+ def handle(self, request: httpx2.Request) -> httpx2.Response:
315
315
  """Match a request against registered routes and return a response.
316
316
 
317
317
  Overrides are checked first, then defaults. Returns a 404 response if no
318
318
  route matches.
319
319
 
320
320
  Args:
321
- request: The incoming httpx.Request to match.
321
+ request: The incoming httpx2.Request to match.
322
322
 
323
323
  Returns:
324
- An httpx.Response from the matched route, or a 404 if unmatched.
324
+ An httpx2.Response from the matched route, or a 404 if unmatched.
325
325
  """
326
326
  method = request.method
327
327
  path = request.url.raw_path.decode("ascii").split("?")[0]
@@ -340,19 +340,19 @@ class _FakeRouter:
340
340
  return self._finalize(route.respond())
341
341
 
342
342
  return self._finalize(
343
- httpx.Response(
343
+ httpx2.Response(
344
344
  status_code=404,
345
345
  json={"detail": f"No route matched: {method} {path}"},
346
346
  )
347
347
  )
348
348
 
349
349
  @staticmethod
350
- def _with_elapsed(response: httpx.Response) -> None:
350
+ def _with_elapsed(response: httpx2.Response) -> None:
351
351
  """Set the elapsed time on a response so BaseService._request can log it."""
352
352
  response.elapsed = datetime.timedelta(milliseconds=1)
353
353
 
354
354
  @staticmethod
355
- def _with_request_id(response: httpx.Response) -> None:
355
+ def _with_request_id(response: httpx2.Response) -> None:
356
356
  """Inject an X-Request-Id header into the response.
357
357
 
358
358
  Reuse an existing request_id from the response body when present so that
@@ -367,7 +367,7 @@ class _FakeRouter:
367
367
  request_id = existing or f"req_{secrets.token_urlsafe(16)}"
368
368
  response.headers[REQUEST_ID_HEADER] = request_id
369
369
 
370
- def _finalize(self, response: httpx.Response) -> httpx.Response:
370
+ def _finalize(self, response: httpx2.Response) -> httpx2.Response:
371
371
  """Apply elapsed time and request ID header to a response."""
372
372
  self._with_elapsed(response)
373
373
  self._with_request_id(response)