valentina-python-client 2.4.1__tar.gz → 2.5.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/PKG-INFO +1 -1
  2. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/pyproject.toml +1 -1
  3. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/__init__.py +1 -1
  4. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/base.py +47 -12
  5. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/character_blueprint.py +38 -3
  6. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/dictionary.py +6 -2
  7. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/constants.py +3 -0
  8. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/base.py +43 -9
  9. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/character_blueprint.py +25 -0
  10. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/dictionary.py +4 -2
  11. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/LICENSE +0 -0
  12. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/README.md +0 -0
  13. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_codegen.py +0 -0
  14. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/__init__.py +0 -0
  15. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/client.py +0 -0
  16. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/registry.py +0 -0
  17. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/__init__.py +0 -0
  18. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/_audit_params.py +0 -0
  19. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/campaign_book_chapters.py +0 -0
  20. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/campaign_books.py +0 -0
  21. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/campaigns.py +0 -0
  22. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/character_autogen.py +0 -0
  23. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/character_traits.py +0 -0
  24. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/characters.py +0 -0
  25. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/companies.py +0 -0
  26. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/developers.py +0 -0
  27. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/dicerolls.py +0 -0
  28. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/global_admin.py +0 -0
  29. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/options.py +0 -0
  30. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/system.py +0 -0
  31. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/user_lookup.py +0 -0
  32. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/user_self_registration.py +0 -0
  33. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/services/users.py +0 -0
  34. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/testing/__init__.py +0 -0
  35. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/_sync/testing/_client.py +0 -0
  36. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/client.py +0 -0
  37. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/config.py +0 -0
  38. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/endpoints.py +0 -0
  39. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/exceptions.py +0 -0
  40. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/__init__.py +0 -0
  41. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/audit_logs.py +0 -0
  42. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/books.py +0 -0
  43. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/campaigns.py +0 -0
  44. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/chapters.py +0 -0
  45. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/character_autogen.py +0 -0
  46. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/character_blueprint.py +0 -0
  47. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/character_trait.py +0 -0
  48. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/characters.py +0 -0
  49. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/companies.py +0 -0
  50. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/developers.py +0 -0
  51. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/diceroll.py +0 -0
  52. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/dictionary.py +0 -0
  53. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/full_sheet.py +0 -0
  54. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/global_admin.py +0 -0
  55. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/pagination.py +0 -0
  56. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/shared.py +0 -0
  57. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/system.py +0 -0
  58. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/user_lookup.py +0 -0
  59. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/models/users.py +0 -0
  60. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/py.typed +0 -0
  61. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/registry.py +0 -0
  62. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/__init__.py +0 -0
  63. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/_audit_params.py +0 -0
  64. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/campaign_book_chapters.py +0 -0
  65. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/campaign_books.py +0 -0
  66. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/campaigns.py +0 -0
  67. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/character_autogen.py +0 -0
  68. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/character_traits.py +0 -0
  69. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/characters.py +0 -0
  70. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/companies.py +0 -0
  71. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/developers.py +0 -0
  72. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/dicerolls.py +0 -0
  73. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/global_admin.py +0 -0
  74. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/options.py +0 -0
  75. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/system.py +0 -0
  76. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/user_lookup.py +0 -0
  77. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/user_self_registration.py +0 -0
  78. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/services/users.py +0 -0
  79. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/testing/__init__.py +0 -0
  80. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/testing/_client.py +0 -0
  81. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/testing/_factories.py +0 -0
  82. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/testing/_router.py +0 -0
  83. {valentina_python_client-2.4.1 → valentina_python_client-2.5.0}/src/vclient/testing/_routes.py +0 -0
  84. {valentina_python_client-2.4.1 → valentina_python_client-2.5.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.4.1
3
+ Version: 2.5.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 @@
16
16
  name = "valentina-python-client"
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.13"
19
- version = "2.4.1"
19
+ version = "2.5.0"
20
20
 
21
21
  [project.optional-dependencies]
22
22
  testing = ["polyfactory>=3.3.0"]
@@ -116,4 +116,4 @@ __all__ = (
116
116
  "users_service",
117
117
  )
118
118
 
119
- __version__ = "2.4.1"
119
+ __version__ = "2.5.0"
@@ -168,7 +168,7 @@ class SyncBaseService:
168
168
  config = self._client._config
169
169
  max_attempts = config.max_retries + 1 if config.auto_retry_rate_limit else 1
170
170
  retry_statuses = config.retry_statuses
171
- request_logger = logger.bind(method=method, url=path)
171
+ request_logger = logger.bind(method=method, url=path, params=params)
172
172
  request_logger.trace("Send request")
173
173
  last_error: RateLimitError | ServerError | None = None
174
174
  for attempt in range(max_attempts):
@@ -196,7 +196,7 @@ class SyncBaseService:
196
196
  time.sleep(delay)
197
197
  continue
198
198
  try:
199
- self._raise_for_status(response, method, path)
199
+ self._raise_for_status(response, method, path, params=params)
200
200
  self._log_success_response(response, request_logger)
201
201
  return response
202
202
  except RateLimitError as e:
@@ -260,13 +260,17 @@ class SyncBaseService:
260
260
  if header_id:
261
261
  response_data["request_id"] = header_id
262
262
 
263
- def _raise_for_status(self, response: httpx.Response, method: str, url: str) -> None:
263
+ def _raise_for_status(
264
+ self, response: httpx.Response, method: str, url: str, params: dict[str, Any] | None = None
265
+ ) -> None:
264
266
  """Raise appropriate exception for error responses.
265
267
 
266
268
  Args:
267
269
  response: The HTTP response to check.
268
270
  method: The HTTP method of the request.
269
271
  url: The URL path of the request.
272
+ params: The query parameters of the request, logged to disambiguate
273
+ paginated calls that share the same path.
270
274
 
271
275
  Raises:
272
276
  AuthenticationError: For 401 responses.
@@ -289,7 +293,11 @@ class SyncBaseService:
289
293
  message = response.text or f"HTTP {status_code}"
290
294
  self._inject_request_id_fallback(response_data, response)
291
295
  error_logger = logger.bind(
292
- method=method, url=url, status=status_code, request_id=response_data.get("request_id")
296
+ method=method,
297
+ url=url,
298
+ params=params,
299
+ status=status_code,
300
+ request_id=response_data.get("request_id"),
293
301
  )
294
302
  if status_code == 429:
295
303
  retry_after = self._parse_retry_after(response)
@@ -584,20 +592,23 @@ class SyncBaseService:
584
592
  limit: int = DEFAULT_PAGE_LIMIT,
585
593
  offset: int = 0,
586
594
  params: dict[str, Any] | None = None,
595
+ max_limit: int = MAX_PAGE_LIMIT,
587
596
  ) -> PaginatedResponse[dict[str, Any]]:
588
597
  """Make a paginated GET request.
589
598
 
590
599
  Args:
591
600
  path: API endpoint path.
592
- limit: Maximum number of items to return (0-100, default 10).
601
+ limit: Maximum number of items to return (default 10).
593
602
  offset: Number of items to skip from the beginning (default 0).
594
603
  params: Additional query parameters.
604
+ max_limit: Upper bound the limit is clamped to (default 100). Reference/catalog
605
+ endpoints pass a higher bound (MAX_REFERENCE_PAGE_LIMIT).
595
606
 
596
607
  Returns:
597
608
  A PaginatedResponse containing the items and pagination metadata.
598
609
  """
599
610
  request_params = {
600
- "limit": min(max(limit, 0), MAX_PAGE_LIMIT),
611
+ "limit": min(max(limit, 0), max_limit),
601
612
  "offset": max(offset, 0),
602
613
  **(params or {}),
603
614
  }
@@ -612,20 +623,25 @@ class SyncBaseService:
612
623
  limit: int = DEFAULT_PAGE_LIMIT,
613
624
  offset: int = 0,
614
625
  params: dict[str, Any] | None = None,
626
+ max_limit: int = MAX_PAGE_LIMIT,
615
627
  ) -> PaginatedResponse[T]:
616
628
  """Make a paginated GET request and parse items into the given model class.
617
629
 
618
630
  Args:
619
631
  path: API endpoint path.
620
632
  model_class: Pydantic model class to validate each item into.
621
- limit: Maximum number of items to return (0-100, default 10).
633
+ limit: Maximum number of items to return (default 10).
622
634
  offset: Number of items to skip from the beginning (default 0).
623
635
  params: Additional query parameters.
636
+ max_limit: Upper bound the limit is clamped to (default 100). Reference/catalog
637
+ endpoints pass a higher bound (MAX_REFERENCE_PAGE_LIMIT).
624
638
 
625
639
  Returns:
626
640
  A PaginatedResponse containing validated model instances.
627
641
  """
628
- response = self._get_paginated(path, limit=limit, offset=offset, params=params)
642
+ response = self._get_paginated(
643
+ path, limit=limit, offset=offset, params=params, max_limit=max_limit
644
+ )
629
645
  return PaginatedResponse(
630
646
  items=[model_class.model_validate(item) for item in response.items],
631
647
  limit=response.limit,
@@ -634,7 +650,12 @@ class SyncBaseService:
634
650
  )
635
651
 
636
652
  def _iter_all_pages(
637
- self, path: str, *, limit: int = MAX_PAGE_LIMIT, params: dict[str, Any] | None = None
653
+ self,
654
+ path: str,
655
+ *,
656
+ limit: int = MAX_PAGE_LIMIT,
657
+ params: dict[str, Any] | None = None,
658
+ max_limit: int = MAX_PAGE_LIMIT,
638
659
  ) -> Iterator[dict[str, Any]]:
639
660
  """Iterate through all pages of a paginated endpoint.
640
661
 
@@ -645,6 +666,8 @@ class SyncBaseService:
645
666
  path: API endpoint path.
646
667
  limit: Items per page (default 100 for efficiency).
647
668
  params: Additional query parameters.
669
+ max_limit: Upper bound the per-page limit is clamped to (default 100).
670
+ Reference/catalog endpoints pass a higher bound (MAX_REFERENCE_PAGE_LIMIT).
648
671
 
649
672
  Yields:
650
673
  Individual items from the paginated response.
@@ -657,7 +680,9 @@ class SyncBaseService:
657
680
  """
658
681
  offset = 0
659
682
  while True:
660
- page = self._get_paginated(path, limit=limit, offset=offset, params=params)
683
+ page = self._get_paginated(
684
+ path, limit=limit, offset=offset, params=params, max_limit=max_limit
685
+ )
661
686
  for item in page.items:
662
687
  yield item
663
688
  if not page.has_more:
@@ -665,7 +690,12 @@ class SyncBaseService:
665
690
  offset = page.next_offset
666
691
 
667
692
  def _get_all(
668
- self, path: str, *, limit: int = MAX_PAGE_LIMIT, params: dict[str, Any] | None = None
693
+ self,
694
+ path: str,
695
+ *,
696
+ limit: int = MAX_PAGE_LIMIT,
697
+ params: dict[str, Any] | None = None,
698
+ max_limit: int = MAX_PAGE_LIMIT,
669
699
  ) -> list[dict[str, Any]]:
670
700
  """Fetch all items from a paginated endpoint.
671
701
 
@@ -677,8 +707,13 @@ class SyncBaseService:
677
707
  path: API endpoint path.
678
708
  limit: Items per page (default 100 for efficiency).
679
709
  params: Additional query parameters.
710
+ max_limit: Upper bound the per-page limit is clamped to (default 100).
711
+ Reference/catalog endpoints pass a higher bound (MAX_REFERENCE_PAGE_LIMIT).
680
712
 
681
713
  Returns:
682
714
  A list of all items from all pages.
683
715
  """
684
- return [item for item in self._iter_all_pages(path, limit=limit, params=params)]
716
+ return [
717
+ item
718
+ for item in self._iter_all_pages(path, limit=limit, params=params, max_limit=max_limit)
719
+ ]
@@ -5,7 +5,13 @@ from collections.abc import Iterator
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from vclient._sync.services.base import SyncBaseService
8
- from vclient.constants import DEFAULT_PAGE_LIMIT, BlueprintTraitOrderBy, CharacterClass, GameVersion
8
+ from vclient.constants import (
9
+ DEFAULT_PAGE_LIMIT,
10
+ MAX_REFERENCE_PAGE_LIMIT,
11
+ BlueprintTraitOrderBy,
12
+ CharacterClass,
13
+ GameVersion,
14
+ )
9
15
  from vclient.endpoints import Endpoints
10
16
  from vclient.models import (
11
17
  CharacterConcept,
@@ -58,6 +64,7 @@ class SyncCharacterBlueprintService(SyncBaseService):
58
64
  SheetSection,
59
65
  limit=limit,
60
66
  offset=offset,
67
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
61
68
  params=self._build_params(game_version=game_version, character_class=character_class),
62
69
  )
63
70
 
@@ -84,6 +91,8 @@ class SyncCharacterBlueprintService(SyncBaseService):
84
91
  """Iterate through all character blueprint sections."""
85
92
  for section in self._iter_all_pages(
86
93
  self._format_endpoint(Endpoints.BLUEPRINT_SECTIONS),
94
+ limit=MAX_REFERENCE_PAGE_LIMIT,
95
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
87
96
  params=self._build_params(game_version=game_version, character_class=character_class),
88
97
  ):
89
98
  yield SheetSection.model_validate(section)
@@ -110,6 +119,7 @@ class SyncCharacterBlueprintService(SyncBaseService):
110
119
  TraitCategory,
111
120
  limit=limit,
112
121
  offset=offset,
122
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
113
123
  params=self._build_params(
114
124
  game_version=game_version, section_id=section_id, character_class=character_class
115
125
  ),
@@ -140,6 +150,8 @@ class SyncCharacterBlueprintService(SyncBaseService):
140
150
  """Iterate through all character blueprint categories."""
141
151
  for category in self._iter_all_pages(
142
152
  self._format_endpoint(Endpoints.BLUEPRINT_CATEGORIES),
153
+ limit=MAX_REFERENCE_PAGE_LIMIT,
154
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
143
155
  params=self._build_params(
144
156
  game_version=game_version, section_id=section_id, character_class=character_class
145
157
  ),
@@ -168,6 +180,7 @@ class SyncCharacterBlueprintService(SyncBaseService):
168
180
  TraitSubcategory,
169
181
  limit=limit,
170
182
  offset=offset,
183
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
171
184
  params=self._build_params(
172
185
  game_version=game_version, category_id=category_id, character_class=character_class
173
186
  ),
@@ -198,6 +211,8 @@ class SyncCharacterBlueprintService(SyncBaseService):
198
211
  """Iterate through all character blueprint subcategories."""
199
212
  for subcategory in self._iter_all_pages(
200
213
  self._format_endpoint(Endpoints.BLUEPRINT_SUBCATEGORIES),
214
+ limit=MAX_REFERENCE_PAGE_LIMIT,
215
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
201
216
  params=self._build_params(
202
217
  game_version=game_version, category_id=category_id, character_class=character_class
203
218
  ),
@@ -232,6 +247,7 @@ class SyncCharacterBlueprintService(SyncBaseService):
232
247
  Trait,
233
248
  limit=limit,
234
249
  offset=offset,
250
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
235
251
  params=self._build_params(
236
252
  character_class=character_class,
237
253
  category_id=category_id,
@@ -282,6 +298,8 @@ class SyncCharacterBlueprintService(SyncBaseService):
282
298
  """Iterate through all character blueprint traits."""
283
299
  for trait in self._iter_all_pages(
284
300
  self._format_endpoint(Endpoints.BLUEPRINT_TRAITS),
301
+ limit=MAX_REFERENCE_PAGE_LIMIT,
302
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
285
303
  params=self._build_params(
286
304
  character_class=character_class,
287
305
  category_id=category_id,
@@ -306,7 +324,11 @@ class SyncCharacterBlueprintService(SyncBaseService):
306
324
  ) -> PaginatedResponse[CharacterConcept]:
307
325
  """Get a paginated page of character concepts."""
308
326
  return self._get_paginated_as(
309
- self._format_endpoint(Endpoints.CONCEPTS), CharacterConcept, limit=limit, offset=offset
327
+ self._format_endpoint(Endpoints.CONCEPTS),
328
+ CharacterConcept,
329
+ limit=limit,
330
+ offset=offset,
331
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
310
332
  )
311
333
 
312
334
  def list_all_concepts(self) -> list[CharacterConcept]:
@@ -315,7 +337,11 @@ class SyncCharacterBlueprintService(SyncBaseService):
315
337
 
316
338
  def iter_all_concepts(self) -> Iterator[CharacterConcept]:
317
339
  """Iterate through all character concepts."""
318
- for concept in self._iter_all_pages(self._format_endpoint(Endpoints.CONCEPTS)):
340
+ for concept in self._iter_all_pages(
341
+ self._format_endpoint(Endpoints.CONCEPTS),
342
+ limit=MAX_REFERENCE_PAGE_LIMIT,
343
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
344
+ ):
319
345
  yield CharacterConcept.model_validate(concept)
320
346
 
321
347
  def get_concept(self, *, concept_id: str) -> CharacterConcept:
@@ -336,6 +362,7 @@ class SyncCharacterBlueprintService(SyncBaseService):
336
362
  VampireClan,
337
363
  limit=limit,
338
364
  offset=offset,
365
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
339
366
  params=self._build_params(game_version=game_version),
340
367
  )
341
368
 
@@ -351,6 +378,8 @@ class SyncCharacterBlueprintService(SyncBaseService):
351
378
  """Iterate through all vampire clans."""
352
379
  for clan in self._iter_all_pages(
353
380
  self._format_endpoint(Endpoints.VAMPIRE_CLANS),
381
+ limit=MAX_REFERENCE_PAGE_LIMIT,
382
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
354
383
  params=self._build_params(game_version=game_version),
355
384
  ):
356
385
  yield VampireClan.model_validate(clan)
@@ -375,6 +404,7 @@ class SyncCharacterBlueprintService(SyncBaseService):
375
404
  WerewolfAuspice,
376
405
  limit=limit,
377
406
  offset=offset,
407
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
378
408
  params=self._build_params(game_version=game_version),
379
409
  )
380
410
 
@@ -390,6 +420,8 @@ class SyncCharacterBlueprintService(SyncBaseService):
390
420
  """Iterate through all werewolf auspices."""
391
421
  for auspice in self._iter_all_pages(
392
422
  self._format_endpoint(Endpoints.WEREWOLF_AUSPICES),
423
+ limit=MAX_REFERENCE_PAGE_LIMIT,
424
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
393
425
  params=self._build_params(game_version=game_version),
394
426
  ):
395
427
  yield WerewolfAuspice.model_validate(auspice)
@@ -416,6 +448,7 @@ class SyncCharacterBlueprintService(SyncBaseService):
416
448
  WerewolfTribe,
417
449
  limit=limit,
418
450
  offset=offset,
451
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
419
452
  params=self._build_params(game_version=game_version),
420
453
  )
421
454
 
@@ -431,6 +464,8 @@ class SyncCharacterBlueprintService(SyncBaseService):
431
464
  """Iterate through all werewolf tribes."""
432
465
  for tribe in self._iter_all_pages(
433
466
  self._format_endpoint(Endpoints.WEREWOLF_TRIBES),
467
+ limit=MAX_REFERENCE_PAGE_LIMIT,
468
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
434
469
  params=self._build_params(game_version=game_version),
435
470
  ):
436
471
  yield WerewolfTribe.model_validate(tribe)
@@ -5,7 +5,7 @@ from collections.abc import Iterator
5
5
  from typing import TYPE_CHECKING
6
6
 
7
7
  from vclient._sync.services.base import SyncBaseService
8
- from vclient.constants import DEFAULT_PAGE_LIMIT
8
+ from vclient.constants import DEFAULT_PAGE_LIMIT, MAX_REFERENCE_PAGE_LIMIT
9
9
  from vclient.endpoints import Endpoints
10
10
  from vclient.models import (
11
11
  DictionaryTerm,
@@ -48,6 +48,7 @@ class SyncDictionaryService(SyncBaseService):
48
48
  DictionaryTerm,
49
49
  limit=limit,
50
50
  offset=offset,
51
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
51
52
  params=self._build_params(term=term),
52
53
  )
53
54
 
@@ -55,11 +56,14 @@ class SyncDictionaryService(SyncBaseService):
55
56
  """Retrieve all dictionary terms."""
56
57
  return [term for term in self.iter_all(term=term)]
57
58
 
58
- def iter_all(self, *, term: str | None = None, limit: int = 100) -> Iterator[DictionaryTerm]:
59
+ def iter_all(
60
+ self, *, term: str | None = None, limit: int = MAX_REFERENCE_PAGE_LIMIT
61
+ ) -> Iterator[DictionaryTerm]:
59
62
  """Iterate through all dictionary terms."""
60
63
  for item in self._iter_all_pages(
61
64
  self._format_endpoint(Endpoints.DICTIONARY_TERMS),
62
65
  limit=limit,
66
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
63
67
  params=self._build_params(term=term),
64
68
  ):
65
69
  yield DictionaryTerm.model_validate(item)
@@ -20,6 +20,9 @@ IDEMPOTENT_HTTP_METHODS: frozenset[str] = frozenset({"GET", "PUT", "DELETE"})
20
20
  # Pagination defaults
21
21
  DEFAULT_PAGE_LIMIT = 10
22
22
  MAX_PAGE_LIMIT = 100
23
+ # Reference/catalog list endpoints serve bounded seed data and allow fetching a
24
+ # full catalog in one request, so they accept a higher per-request limit.
25
+ MAX_REFERENCE_PAGE_LIMIT = 1000
23
26
 
24
27
  # Server log tail defaults
25
28
  DEFAULT_LOG_TAIL_LIMIT = 100
@@ -176,7 +176,9 @@ class BaseService:
176
176
  config = self._client._config # noqa: SLF001
177
177
  max_attempts = config.max_retries + 1 if config.auto_retry_rate_limit else 1
178
178
  retry_statuses = config.retry_statuses
179
- request_logger = logger.bind(method=method, url=path)
179
+ # Bind params so paginated requests are distinguishable in logs; without
180
+ # limit/offset, identical paths look like duplicate (thundering-herd) calls.
181
+ request_logger = logger.bind(method=method, url=path, params=params)
180
182
 
181
183
  # TRACE so the in-flight request is visible only when diagnosing hangs; the
182
184
  # "Request complete" DEBUG line below is the superset record for normal use.
@@ -211,7 +213,7 @@ class BaseService:
211
213
  continue
212
214
 
213
215
  try:
214
- self._raise_for_status(response, method, path)
216
+ self._raise_for_status(response, method, path, params=params)
215
217
  self._log_success_response(response, request_logger)
216
218
  return response # noqa: TRY300
217
219
  except RateLimitError as e:
@@ -287,13 +289,21 @@ class BaseService:
287
289
  if header_id:
288
290
  response_data["request_id"] = header_id
289
291
 
290
- def _raise_for_status(self, response: httpx.Response, method: str, url: str) -> None:
292
+ def _raise_for_status(
293
+ self,
294
+ response: httpx.Response,
295
+ method: str,
296
+ url: str,
297
+ params: dict[str, Any] | None = None,
298
+ ) -> None:
291
299
  """Raise appropriate exception for error responses.
292
300
 
293
301
  Args:
294
302
  response: The HTTP response to check.
295
303
  method: The HTTP method of the request.
296
304
  url: The URL path of the request.
305
+ params: The query parameters of the request, logged to disambiguate
306
+ paginated calls that share the same path.
297
307
 
298
308
  Raises:
299
309
  AuthenticationError: For 401 responses.
@@ -319,7 +329,11 @@ class BaseService:
319
329
  self._inject_request_id_fallback(response_data, response)
320
330
 
321
331
  error_logger = logger.bind(
322
- method=method, url=url, status=status_code, request_id=response_data.get("request_id")
332
+ method=method,
333
+ url=url,
334
+ params=params,
335
+ status=status_code,
336
+ request_id=response_data.get("request_id"),
323
337
  )
324
338
 
325
339
  if status_code == 429: # noqa: PLR2004
@@ -653,20 +667,23 @@ class BaseService:
653
667
  limit: int = DEFAULT_PAGE_LIMIT,
654
668
  offset: int = 0,
655
669
  params: dict[str, Any] | None = None,
670
+ max_limit: int = MAX_PAGE_LIMIT,
656
671
  ) -> PaginatedResponse[dict[str, Any]]:
657
672
  """Make a paginated GET request.
658
673
 
659
674
  Args:
660
675
  path: API endpoint path.
661
- limit: Maximum number of items to return (0-100, default 10).
676
+ limit: Maximum number of items to return (default 10).
662
677
  offset: Number of items to skip from the beginning (default 0).
663
678
  params: Additional query parameters.
679
+ max_limit: Upper bound the limit is clamped to (default 100). Reference/catalog
680
+ endpoints pass a higher bound (MAX_REFERENCE_PAGE_LIMIT).
664
681
 
665
682
  Returns:
666
683
  A PaginatedResponse containing the items and pagination metadata.
667
684
  """
668
685
  request_params = {
669
- "limit": min(max(limit, 0), MAX_PAGE_LIMIT), # Clamp to valid range
686
+ "limit": min(max(limit, 0), max_limit), # Clamp to valid range
670
687
  "offset": max(offset, 0),
671
688
  **(params or {}),
672
689
  }
@@ -682,20 +699,25 @@ class BaseService:
682
699
  limit: int = DEFAULT_PAGE_LIMIT,
683
700
  offset: int = 0,
684
701
  params: dict[str, Any] | None = None,
702
+ max_limit: int = MAX_PAGE_LIMIT,
685
703
  ) -> PaginatedResponse[T]:
686
704
  """Make a paginated GET request and parse items into the given model class.
687
705
 
688
706
  Args:
689
707
  path: API endpoint path.
690
708
  model_class: Pydantic model class to validate each item into.
691
- limit: Maximum number of items to return (0-100, default 10).
709
+ limit: Maximum number of items to return (default 10).
692
710
  offset: Number of items to skip from the beginning (default 0).
693
711
  params: Additional query parameters.
712
+ max_limit: Upper bound the limit is clamped to (default 100). Reference/catalog
713
+ endpoints pass a higher bound (MAX_REFERENCE_PAGE_LIMIT).
694
714
 
695
715
  Returns:
696
716
  A PaginatedResponse containing validated model instances.
697
717
  """
698
- response = await self._get_paginated(path, limit=limit, offset=offset, params=params)
718
+ response = await self._get_paginated(
719
+ path, limit=limit, offset=offset, params=params, max_limit=max_limit
720
+ )
699
721
  return PaginatedResponse(
700
722
  items=[model_class.model_validate(item) for item in response.items],
701
723
  limit=response.limit,
@@ -709,6 +731,7 @@ class BaseService:
709
731
  *,
710
732
  limit: int = MAX_PAGE_LIMIT,
711
733
  params: dict[str, Any] | None = None,
734
+ max_limit: int = MAX_PAGE_LIMIT,
712
735
  ) -> AsyncIterator[dict[str, Any]]:
713
736
  """Iterate through all pages of a paginated endpoint.
714
737
 
@@ -719,6 +742,8 @@ class BaseService:
719
742
  path: API endpoint path.
720
743
  limit: Items per page (default 100 for efficiency).
721
744
  params: Additional query parameters.
745
+ max_limit: Upper bound the per-page limit is clamped to (default 100).
746
+ Reference/catalog endpoints pass a higher bound (MAX_REFERENCE_PAGE_LIMIT).
722
747
 
723
748
  Yields:
724
749
  Individual items from the paginated response.
@@ -737,6 +762,7 @@ class BaseService:
737
762
  limit=limit,
738
763
  offset=offset,
739
764
  params=params,
765
+ max_limit=max_limit,
740
766
  )
741
767
 
742
768
  for item in page.items:
@@ -753,6 +779,7 @@ class BaseService:
753
779
  *,
754
780
  limit: int = MAX_PAGE_LIMIT,
755
781
  params: dict[str, Any] | None = None,
782
+ max_limit: int = MAX_PAGE_LIMIT,
756
783
  ) -> list[dict[str, Any]]:
757
784
  """Fetch all items from a paginated endpoint.
758
785
 
@@ -764,8 +791,15 @@ class BaseService:
764
791
  path: API endpoint path.
765
792
  limit: Items per page (default 100 for efficiency).
766
793
  params: Additional query parameters.
794
+ max_limit: Upper bound the per-page limit is clamped to (default 100).
795
+ Reference/catalog endpoints pass a higher bound (MAX_REFERENCE_PAGE_LIMIT).
767
796
 
768
797
  Returns:
769
798
  A list of all items from all pages.
770
799
  """
771
- return [item async for item in self._iter_all_pages(path, limit=limit, params=params)]
800
+ return [
801
+ item
802
+ async for item in self._iter_all_pages(
803
+ path, limit=limit, params=params, max_limit=max_limit
804
+ )
805
+ ]
@@ -5,6 +5,7 @@ from typing import TYPE_CHECKING
5
5
 
6
6
  from vclient.constants import (
7
7
  DEFAULT_PAGE_LIMIT,
8
+ MAX_REFERENCE_PAGE_LIMIT,
8
9
  BlueprintTraitOrderBy,
9
10
  CharacterClass,
10
11
  GameVersion,
@@ -65,6 +66,7 @@ class CharacterBlueprintService(BaseService):
65
66
  SheetSection,
66
67
  limit=limit,
67
68
  offset=offset,
69
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
68
70
  params=self._build_params(game_version=game_version, character_class=character_class),
69
71
  )
70
72
 
@@ -91,6 +93,8 @@ class CharacterBlueprintService(BaseService):
91
93
  """Iterate through all character blueprint sections."""
92
94
  async for section in self._iter_all_pages(
93
95
  self._format_endpoint(Endpoints.BLUEPRINT_SECTIONS),
96
+ limit=MAX_REFERENCE_PAGE_LIMIT,
97
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
94
98
  params=self._build_params(game_version=game_version, character_class=character_class),
95
99
  ):
96
100
  yield SheetSection.model_validate(section)
@@ -122,6 +126,7 @@ class CharacterBlueprintService(BaseService):
122
126
  TraitCategory,
123
127
  limit=limit,
124
128
  offset=offset,
129
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
125
130
  params=self._build_params(
126
131
  game_version=game_version,
127
132
  section_id=section_id,
@@ -156,6 +161,8 @@ class CharacterBlueprintService(BaseService):
156
161
  """Iterate through all character blueprint categories."""
157
162
  async for category in self._iter_all_pages(
158
163
  self._format_endpoint(Endpoints.BLUEPRINT_CATEGORIES),
164
+ limit=MAX_REFERENCE_PAGE_LIMIT,
165
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
159
166
  params=self._build_params(
160
167
  game_version=game_version,
161
168
  section_id=section_id,
@@ -191,6 +198,7 @@ class CharacterBlueprintService(BaseService):
191
198
  TraitSubcategory,
192
199
  limit=limit,
193
200
  offset=offset,
201
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
194
202
  params=self._build_params(
195
203
  game_version=game_version,
196
204
  category_id=category_id,
@@ -225,6 +233,8 @@ class CharacterBlueprintService(BaseService):
225
233
  """Iterate through all character blueprint subcategories."""
226
234
  async for subcategory in self._iter_all_pages(
227
235
  self._format_endpoint(Endpoints.BLUEPRINT_SUBCATEGORIES),
236
+ limit=MAX_REFERENCE_PAGE_LIMIT,
237
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
228
238
  params=self._build_params(
229
239
  game_version=game_version,
230
240
  category_id=category_id,
@@ -264,6 +274,7 @@ class CharacterBlueprintService(BaseService):
264
274
  Trait,
265
275
  limit=limit,
266
276
  offset=offset,
277
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
267
278
  params=self._build_params(
268
279
  character_class=character_class,
269
280
  category_id=category_id,
@@ -314,6 +325,8 @@ class CharacterBlueprintService(BaseService):
314
325
  """Iterate through all character blueprint traits."""
315
326
  async for trait in self._iter_all_pages(
316
327
  self._format_endpoint(Endpoints.BLUEPRINT_TRAITS),
328
+ limit=MAX_REFERENCE_PAGE_LIMIT,
329
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
317
330
  params=self._build_params(
318
331
  character_class=character_class,
319
332
  category_id=category_id,
@@ -347,6 +360,7 @@ class CharacterBlueprintService(BaseService):
347
360
  CharacterConcept,
348
361
  limit=limit,
349
362
  offset=offset,
363
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
350
364
  )
351
365
 
352
366
  async def list_all_concepts(self) -> list[CharacterConcept]:
@@ -357,6 +371,8 @@ class CharacterBlueprintService(BaseService):
357
371
  """Iterate through all character concepts."""
358
372
  async for concept in self._iter_all_pages(
359
373
  self._format_endpoint(Endpoints.CONCEPTS),
374
+ limit=MAX_REFERENCE_PAGE_LIMIT,
375
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
360
376
  ):
361
377
  yield CharacterConcept.model_validate(concept)
362
378
 
@@ -383,6 +399,7 @@ class CharacterBlueprintService(BaseService):
383
399
  VampireClan,
384
400
  limit=limit,
385
401
  offset=offset,
402
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
386
403
  params=self._build_params(game_version=game_version),
387
404
  )
388
405
 
@@ -398,6 +415,8 @@ class CharacterBlueprintService(BaseService):
398
415
  """Iterate through all vampire clans."""
399
416
  async for clan in self._iter_all_pages(
400
417
  self._format_endpoint(Endpoints.VAMPIRE_CLANS),
418
+ limit=MAX_REFERENCE_PAGE_LIMIT,
419
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
401
420
  params=self._build_params(game_version=game_version),
402
421
  ):
403
422
  yield VampireClan.model_validate(clan)
@@ -425,6 +444,7 @@ class CharacterBlueprintService(BaseService):
425
444
  WerewolfAuspice,
426
445
  limit=limit,
427
446
  offset=offset,
447
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
428
448
  params=self._build_params(game_version=game_version),
429
449
  )
430
450
 
@@ -442,6 +462,8 @@ class CharacterBlueprintService(BaseService):
442
462
  """Iterate through all werewolf auspices."""
443
463
  async for auspice in self._iter_all_pages(
444
464
  self._format_endpoint(Endpoints.WEREWOLF_AUSPICES),
465
+ limit=MAX_REFERENCE_PAGE_LIMIT,
466
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
445
467
  params=self._build_params(game_version=game_version),
446
468
  ):
447
469
  yield WerewolfAuspice.model_validate(auspice)
@@ -471,6 +493,7 @@ class CharacterBlueprintService(BaseService):
471
493
  WerewolfTribe,
472
494
  limit=limit,
473
495
  offset=offset,
496
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
474
497
  params=self._build_params(game_version=game_version),
475
498
  )
476
499
 
@@ -486,6 +509,8 @@ class CharacterBlueprintService(BaseService):
486
509
  """Iterate through all werewolf tribes."""
487
510
  async for tribe in self._iter_all_pages(
488
511
  self._format_endpoint(Endpoints.WEREWOLF_TRIBES),
512
+ limit=MAX_REFERENCE_PAGE_LIMIT,
513
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
489
514
  params=self._build_params(game_version=game_version),
490
515
  ):
491
516
  yield WerewolfTribe.model_validate(tribe)
@@ -3,7 +3,7 @@
3
3
  from collections.abc import AsyncIterator
4
4
  from typing import TYPE_CHECKING
5
5
 
6
- from vclient.constants import DEFAULT_PAGE_LIMIT
6
+ from vclient.constants import DEFAULT_PAGE_LIMIT, MAX_REFERENCE_PAGE_LIMIT
7
7
  from vclient.endpoints import Endpoints
8
8
  from vclient.models import (
9
9
  DictionaryTerm,
@@ -53,6 +53,7 @@ class DictionaryService(BaseService):
53
53
  DictionaryTerm,
54
54
  limit=limit,
55
55
  offset=offset,
56
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
56
57
  params=self._build_params(term=term),
57
58
  )
58
59
 
@@ -61,12 +62,13 @@ class DictionaryService(BaseService):
61
62
  return [term async for term in self.iter_all(term=term)]
62
63
 
63
64
  async def iter_all(
64
- self, *, term: str | None = None, limit: int = 100
65
+ self, *, term: str | None = None, limit: int = MAX_REFERENCE_PAGE_LIMIT
65
66
  ) -> AsyncIterator[DictionaryTerm]:
66
67
  """Iterate through all dictionary terms."""
67
68
  async for item in self._iter_all_pages(
68
69
  self._format_endpoint(Endpoints.DICTIONARY_TERMS),
69
70
  limit=limit,
71
+ max_limit=MAX_REFERENCE_PAGE_LIMIT,
70
72
  params=self._build_params(term=term),
71
73
  ):
72
74
  yield DictionaryTerm.model_validate(item)