valentina-python-client 1.6.1__tar.gz → 1.7.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/PKG-INFO +3 -3
  2. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/README.md +2 -2
  3. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/pyproject.toml +20 -8
  4. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/__init__.py +39 -5
  5. valentina_python_client-1.7.2/src/vclient/_codegen.py +368 -0
  6. valentina_python_client-1.7.2/src/vclient/_sync/__init__.py +45 -0
  7. valentina_python_client-1.7.2/src/vclient/_sync/client.py +542 -0
  8. valentina_python_client-1.7.2/src/vclient/_sync/registry.py +434 -0
  9. valentina_python_client-1.7.2/src/vclient/_sync/services/__init__.py +38 -0
  10. valentina_python_client-1.7.2/src/vclient/_sync/services/base.py +626 -0
  11. valentina_python_client-1.7.2/src/vclient/_sync/services/campaign_book_chapters.py +418 -0
  12. valentina_python_client-1.7.2/src/vclient/_sync/services/campaign_books.py +510 -0
  13. valentina_python_client-1.7.2/src/vclient/_sync/services/campaigns.py +502 -0
  14. valentina_python_client-1.7.2/src/vclient/_sync/services/character_autogen.py +113 -0
  15. valentina_python_client-1.7.2/src/vclient/_sync/services/character_blueprint.py +600 -0
  16. valentina_python_client-1.7.2/src/vclient/_sync/services/character_traits.py +230 -0
  17. valentina_python_client-1.7.2/src/vclient/_sync/services/characters.py +1219 -0
  18. valentina_python_client-1.7.2/src/vclient/_sync/services/companies.py +218 -0
  19. valentina_python_client-1.7.2/src/vclient/_sync/services/developers.py +77 -0
  20. valentina_python_client-1.7.2/src/vclient/_sync/services/dicerolls.py +131 -0
  21. valentina_python_client-1.7.2/src/vclient/_sync/services/dictionary.py +105 -0
  22. valentina_python_client-1.7.2/src/vclient/_sync/services/global_admin.py +199 -0
  23. valentina_python_client-1.7.2/src/vclient/_sync/services/options.py +38 -0
  24. valentina_python_client-1.7.2/src/vclient/_sync/services/system.py +37 -0
  25. valentina_python_client-1.7.2/src/vclient/_sync/services/users.py +818 -0
  26. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/character_trait.py +1 -0
  27. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/character_traits.py +19 -2
  28. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/LICENSE +0 -0
  29. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/client.py +0 -0
  30. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/config.py +0 -0
  31. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/constants.py +0 -0
  32. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/endpoints.py +0 -0
  33. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/exceptions.py +0 -0
  34. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/__init__.py +0 -0
  35. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/books.py +0 -0
  36. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/campaigns.py +0 -0
  37. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/chapters.py +0 -0
  38. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/character_autogen.py +0 -0
  39. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/character_blueprint.py +0 -0
  40. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/characters.py +0 -0
  41. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/companies.py +0 -0
  42. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/developers.py +0 -0
  43. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/diceroll.py +0 -0
  44. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/dictionary.py +0 -0
  45. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/global_admin.py +0 -0
  46. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/pagination.py +0 -0
  47. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/shared.py +0 -0
  48. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/system.py +0 -0
  49. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/models/users.py +0 -0
  50. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/py.typed +0 -0
  51. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/registry.py +0 -0
  52. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/__init__.py +0 -0
  53. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/base.py +0 -0
  54. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/campaign_book_chapters.py +0 -0
  55. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/campaign_books.py +0 -0
  56. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/campaigns.py +0 -0
  57. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/character_autogen.py +0 -0
  58. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/character_blueprint.py +0 -0
  59. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/characters.py +0 -0
  60. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/companies.py +0 -0
  61. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/developers.py +0 -0
  62. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/dicerolls.py +0 -0
  63. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/dictionary.py +0 -0
  64. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/global_admin.py +0 -0
  65. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/options.py +0 -0
  66. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/system.py +0 -0
  67. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/src/vclient/services/users.py +0 -0
  68. {valentina_python_client-1.6.1 → valentina_python_client-1.7.2}/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.6.1
3
+ Version: 1.7.2
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>
@@ -26,11 +26,11 @@ Description-Content-Type: text/markdown
26
26
 
27
27
  # Valentina Python Client
28
28
 
29
- Async Python client library for accessing the Valentina Noir API.
29
+ Async and sync Python client library for accessing the Valentina Noir API.
30
30
 
31
31
  ## Features
32
32
 
33
- - **Async-first design** - Built on httpx for efficient async HTTP operations
33
+ - **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on httpx
34
34
  - **Type-safe** - Full type hints with Pydantic models for request/response validation
35
35
  - **Convenient factory pattern** - Create a client once, access services from anywhere
36
36
  - **Automatic pagination** - Stream through large datasets with `iter_all()` or fetch everything with `list_all()`
@@ -1,10 +1,10 @@
1
1
  # Valentina Python Client
2
2
 
3
- Async Python client library for accessing the Valentina Noir API.
3
+ Async and sync Python client library for accessing the Valentina Noir API.
4
4
 
5
5
  ## Features
6
6
 
7
- - **Async-first design** - Built on httpx for efficient async HTTP operations
7
+ - **Async and sync clients** - Both `VClient` (async) and `SyncVClient` (sync) built on httpx
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()`
@@ -10,7 +10,7 @@
10
10
  name = "valentina-python-client"
11
11
  readme = "README.md"
12
12
  requires-python = ">=3.13"
13
- version = "1.6.1"
13
+ version = "1.7.2"
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.8",
28
+ "commitizen>=4.13.9",
29
29
  "coverage>=7.13.4",
30
30
  "duty>=1.9.0",
31
- "prek>=0.3.3",
31
+ "prek>=0.3.4",
32
32
  "pytest-anyio>=0.0.0",
33
33
  "pytest-clarity>=1.0.1",
34
34
  "pytest-cov>=7.0.0",
@@ -39,14 +39,14 @@
39
39
  "pytest-xdist>=3.8.0",
40
40
  "pytest>=9.0.2",
41
41
  "respx>=0.22.0",
42
- "ruff>=0.15.2",
42
+ "ruff>=0.15.4",
43
43
  "shellcheck-py>=0.11.0.1",
44
- "ty>=0.0.17",
45
- "typos>=1.43.5",
44
+ "ty>=0.0.19",
45
+ "typos>=1.44.0",
46
46
  "vulture>=2.14",
47
47
  "yamllint>=1.38.0",
48
48
  ]
49
- docs = ["zensical>=0.0.23"]
49
+ docs = ["zensical>=0.0.24"]
50
50
 
51
51
  [tool.commitizen]
52
52
  bump_message = "bump(release): v$current_version → v$new_version"
@@ -121,7 +121,18 @@
121
121
  "TD002", # Missing author in TODO
122
122
  "TD003", # Missing issue link on the line following this TODO
123
123
  ]
124
- per-file-ignores = { "scripts/*.py" = [
124
+ per-file-ignores = { "src/vclient/_sync/**" = [
125
+ "C416", # Unnecessary list comprehension (AST unparser artifact)
126
+ "D", # Docstring formatting (AST unparser doesn't preserve style)
127
+ "E501", # Line too long
128
+ "ERA001", # Commented-out code false positives
129
+ "PLR0913", # Too many arguments
130
+ "PLR2004", # Magic value used in comparison
131
+ "PLW0603", # Using global statement
132
+ "SLF001", # Private member accessed
133
+ "TRY300", # Consider moving to else block
134
+ "UP028", # Replace yield over for loop
135
+ ], "scripts/*.py" = [
125
136
  "INP001", # Implicit namespace package (no __init__.py needed)
126
137
  "T201", # print() is intentional for CLI output
127
138
  ], "src/vclient/services/*.py" = [
@@ -173,6 +184,7 @@
173
184
  max-args = 6
174
185
 
175
186
  [tool.ruff.format]
187
+ exclude = ["src/vclient/_sync"]
176
188
  indent-style = "space"
177
189
  line-ending = "auto"
178
190
  quote-style = "double"
@@ -1,10 +1,12 @@
1
1
  """API client for the vclient service.
2
2
 
3
3
  Primary exports:
4
- VClient: The main API client class
4
+ VClient: The main async API client class
5
+ SyncVClient: The synchronous API client class
5
6
 
6
7
  Factory functions (primary access pattern):
7
- books_service, campaigns_service, chapters_service, etc.
8
+ Async: books_service, campaigns_service, chapters_service, etc.
9
+ Sync: sync_books_service, sync_campaigns_service, sync_chapters_service, etc.
8
10
 
9
11
  For exceptions, use: from vclient.exceptions import APIError, NotFoundError, ...
10
12
  For models, use: from vclient.models import Character, Campaign, ...
@@ -31,6 +33,24 @@ _logger.add(
31
33
 
32
34
  _logger.disable("vclient")
33
35
 
36
+ from vclient._sync import ( # noqa: E402
37
+ SyncVClient,
38
+ sync_books_service,
39
+ sync_campaigns_service,
40
+ sync_chapters_service,
41
+ sync_character_autogen_service,
42
+ sync_character_blueprint_service,
43
+ sync_character_traits_service,
44
+ sync_characters_service,
45
+ sync_companies_service,
46
+ sync_developer_service,
47
+ sync_dicerolls_service,
48
+ sync_dictionary_service,
49
+ sync_global_admin_service,
50
+ sync_options_service,
51
+ sync_system_service,
52
+ sync_users_service,
53
+ )
34
54
  from vclient.client import VClient # noqa: E402
35
55
  from vclient.registry import ( # noqa: E402
36
56
  books_service,
@@ -51,9 +71,8 @@ from vclient.registry import ( # noqa: E402
51
71
  )
52
72
 
53
73
  __all__ = (
54
- # Core
74
+ "SyncVClient",
55
75
  "VClient",
56
- # Factory functions
57
76
  "books_service",
58
77
  "campaigns_service",
59
78
  "chapters_service",
@@ -67,8 +86,23 @@ __all__ = (
67
86
  "dictionary_service",
68
87
  "global_admin_service",
69
88
  "options_service",
89
+ "sync_books_service",
90
+ "sync_campaigns_service",
91
+ "sync_chapters_service",
92
+ "sync_character_autogen_service",
93
+ "sync_character_blueprint_service",
94
+ "sync_character_traits_service",
95
+ "sync_characters_service",
96
+ "sync_companies_service",
97
+ "sync_developer_service",
98
+ "sync_dicerolls_service",
99
+ "sync_dictionary_service",
100
+ "sync_global_admin_service",
101
+ "sync_options_service",
102
+ "sync_system_service",
103
+ "sync_users_service",
70
104
  "system_service",
71
105
  "users_service",
72
106
  )
73
107
 
74
- __version__ = "1.6.1"
108
+ __version__ = "1.7.2"
@@ -0,0 +1,368 @@
1
+ """AST-based code generator that transforms async source files into synchronous equivalents.
2
+
3
+ Reads the async vclient source as the single source of truth and mechanically
4
+ produces a ``vclient._sync`` package with all async constructs removed.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import ast
10
+ import sys
11
+ from pathlib import Path
12
+
13
+ HEADER_COMMENT = "# AUTO-GENERATED — do not edit. Run 'uv run duty generate_sync' to regenerate.\n"
14
+
15
+ RENAME_CLASSES: dict[str, str] = {
16
+ "BaseService": "SyncBaseService",
17
+ "BooksService": "SyncBooksService",
18
+ "CampaignsService": "SyncCampaignsService",
19
+ "ChaptersService": "SyncChaptersService",
20
+ "CharacterAutogenService": "SyncCharacterAutogenService",
21
+ "CharacterBlueprintService": "SyncCharacterBlueprintService",
22
+ "CharacterTraitsService": "SyncCharacterTraitsService",
23
+ "CharactersService": "SyncCharactersService",
24
+ "CompaniesService": "SyncCompaniesService",
25
+ "DeveloperService": "SyncDeveloperService",
26
+ "DicerollService": "SyncDicerollService",
27
+ "DictionaryService": "SyncDictionaryService",
28
+ "GlobalAdminService": "SyncGlobalAdminService",
29
+ "OptionsService": "SyncOptionsService",
30
+ "SystemService": "SyncSystemService",
31
+ "UsersService": "SyncUsersService",
32
+ "VClient": "SyncVClient",
33
+ }
34
+
35
+ FACTORY_RENAMES: dict[str, str] = {
36
+ "companies_service": "sync_companies_service",
37
+ "developer_service": "sync_developer_service",
38
+ "global_admin_service": "sync_global_admin_service",
39
+ "system_service": "sync_system_service",
40
+ "users_service": "sync_users_service",
41
+ "campaigns_service": "sync_campaigns_service",
42
+ "books_service": "sync_books_service",
43
+ "chapters_service": "sync_chapters_service",
44
+ "characters_service": "sync_characters_service",
45
+ "character_traits_service": "sync_character_traits_service",
46
+ "character_blueprint_service": "sync_character_blueprint_service",
47
+ "dictionary_service": "sync_dictionary_service",
48
+ "dicerolls_service": "sync_dicerolls_service",
49
+ "options_service": "sync_options_service",
50
+ "character_autogen_service": "sync_character_autogen_service",
51
+ "configure_default_client": "sync_configure_default_client",
52
+ "clear_default_client": "sync_clear_default_client",
53
+ "default_client": "sync_default_client",
54
+ }
55
+
56
+ IMPORT_REWRITES: dict[str, str] = {
57
+ "vclient.client": "vclient._sync.client",
58
+ "vclient.services": "vclient._sync.services",
59
+ "vclient.services.base": "vclient._sync.services.base",
60
+ "vclient.services.companies": "vclient._sync.services.companies",
61
+ "vclient.services.developers": "vclient._sync.services.developers",
62
+ "vclient.services.global_admin": "vclient._sync.services.global_admin",
63
+ "vclient.services.system": "vclient._sync.services.system",
64
+ "vclient.services.users": "vclient._sync.services.users",
65
+ "vclient.services.campaigns": "vclient._sync.services.campaigns",
66
+ "vclient.services.campaign_books": "vclient._sync.services.campaign_books",
67
+ "vclient.services.campaign_book_chapters": "vclient._sync.services.campaign_book_chapters",
68
+ "vclient.services.characters": "vclient._sync.services.characters",
69
+ "vclient.services.character_traits": "vclient._sync.services.character_traits",
70
+ "vclient.services.character_blueprint": "vclient._sync.services.character_blueprint",
71
+ "vclient.services.dictionary": "vclient._sync.services.dictionary",
72
+ "vclient.services.dicerolls": "vclient._sync.services.dicerolls",
73
+ "vclient.services.options": "vclient._sync.services.options",
74
+ "vclient.services.character_autogen": "vclient._sync.services.character_autogen",
75
+ "vclient.registry": "vclient._sync.registry",
76
+ }
77
+
78
+ # Combined lookup for renaming any identifier (class or factory function)
79
+ _ALL_RENAMES: dict[str, str] = {**RENAME_CLASSES, **FACTORY_RENAMES}
80
+
81
+
82
+ class AsyncToSyncTransformer(ast.NodeTransformer):
83
+ """Transform async Python source into synchronous equivalents.
84
+
85
+ Visit each relevant AST node and mechanically strip or rename async
86
+ constructs so the resulting tree is fully synchronous.
87
+ """
88
+
89
+ # ------------------------------------------------------------------
90
+ # Async construct removal
91
+ # ------------------------------------------------------------------
92
+
93
+ def visit_AsyncFunctionDef(self, node: ast.AsyncFunctionDef) -> ast.FunctionDef:
94
+ """Convert ``async def`` to ``def``, renaming dunder methods."""
95
+ self.generic_visit(node)
96
+
97
+ name = node.name
98
+ if name == "__aenter__":
99
+ name = "__enter__"
100
+ elif name == "__aexit__":
101
+ name = "__exit__"
102
+ else:
103
+ name = FACTORY_RENAMES.get(name, name)
104
+
105
+ return ast.FunctionDef(
106
+ name=name,
107
+ args=node.args,
108
+ body=node.body,
109
+ decorator_list=node.decorator_list,
110
+ returns=node.returns,
111
+ type_comment=node.type_comment,
112
+ )
113
+
114
+ def visit_Await(self, node: ast.Await) -> ast.expr:
115
+ """Unwrap ``await expr`` to just ``expr``."""
116
+ self.generic_visit(node)
117
+ return node.value
118
+
119
+ def visit_AsyncWith(self, node: ast.AsyncWith) -> ast.With:
120
+ """Convert ``async with`` to ``with``."""
121
+ self.generic_visit(node)
122
+ return ast.With(items=node.items, body=node.body)
123
+
124
+ def visit_AsyncFor(self, node: ast.AsyncFor) -> ast.For:
125
+ """Convert ``async for`` to ``for``."""
126
+ self.generic_visit(node)
127
+ return ast.For(
128
+ target=node.target,
129
+ iter=node.iter,
130
+ body=node.body,
131
+ orelse=node.orelse,
132
+ )
133
+
134
+ # ------------------------------------------------------------------
135
+ # Async comprehensions
136
+ # ------------------------------------------------------------------
137
+
138
+ def _sync_generators(self, generators: list[ast.comprehension]) -> None:
139
+ """Set ``is_async = 0`` on all generators in-place."""
140
+ for gen in generators:
141
+ gen.is_async = 0
142
+
143
+ def visit_ListComp(self, node: ast.ListComp) -> ast.ListComp:
144
+ """Remove async from list comprehension generators."""
145
+ self.generic_visit(node)
146
+ self._sync_generators(node.generators)
147
+ return node
148
+
149
+ def visit_SetComp(self, node: ast.SetComp) -> ast.SetComp:
150
+ """Remove async from set comprehension generators."""
151
+ self.generic_visit(node)
152
+ self._sync_generators(node.generators)
153
+ return node
154
+
155
+ def visit_GeneratorExp(self, node: ast.GeneratorExp) -> ast.GeneratorExp:
156
+ """Remove async from generator expression generators."""
157
+ self.generic_visit(node)
158
+ self._sync_generators(node.generators)
159
+ return node
160
+
161
+ # ------------------------------------------------------------------
162
+ # Name / Attribute renaming
163
+ # ------------------------------------------------------------------
164
+
165
+ def visit_Name(self, node: ast.Name) -> ast.Name:
166
+ """Rename ``AsyncIterator`` to ``Iterator`` and service class names."""
167
+ self.generic_visit(node)
168
+ if node.id == "AsyncIterator":
169
+ node.id = "Iterator"
170
+ elif node.id in _ALL_RENAMES:
171
+ node.id = _ALL_RENAMES[node.id]
172
+ return node
173
+
174
+ def visit_Attribute(self, node: ast.Attribute) -> ast.Attribute:
175
+ """Rename ``AsyncClient`` to ``Client`` and ``aclose`` to ``close``."""
176
+ self.generic_visit(node)
177
+ if node.attr == "AsyncClient":
178
+ node.attr = "Client"
179
+ elif node.attr == "aclose":
180
+ node.attr = "close"
181
+ elif node.attr in _ALL_RENAMES:
182
+ node.attr = _ALL_RENAMES[node.attr]
183
+ return node
184
+
185
+ # ------------------------------------------------------------------
186
+ # Class / function definition renaming
187
+ # ------------------------------------------------------------------
188
+
189
+ def visit_ClassDef(self, node: ast.ClassDef) -> ast.ClassDef:
190
+ """Rename class definitions found in RENAME_CLASSES."""
191
+ self.generic_visit(node)
192
+ if node.name in RENAME_CLASSES:
193
+ node.name = RENAME_CLASSES[node.name]
194
+ return node
195
+
196
+ def visit_FunctionDef(self, node: ast.FunctionDef) -> ast.FunctionDef:
197
+ """Rename factory function definitions found in FACTORY_RENAMES."""
198
+ self.generic_visit(node)
199
+ if node.name in FACTORY_RENAMES:
200
+ node.name = FACTORY_RENAMES[node.name]
201
+ return node
202
+
203
+ # ------------------------------------------------------------------
204
+ # String annotations (TYPE_CHECKING)
205
+ # ------------------------------------------------------------------
206
+
207
+ def visit_Constant(self, node: ast.Constant) -> ast.Constant:
208
+ """Rename class names inside string annotations."""
209
+ if isinstance(node.value, str):
210
+ value = node.value
211
+ for old, new in _ALL_RENAMES.items():
212
+ value = value.replace(old, new)
213
+ node.value = value
214
+ return node
215
+
216
+ # ------------------------------------------------------------------
217
+ # Import rewriting
218
+ # ------------------------------------------------------------------
219
+
220
+ def visit_ImportFrom(self, node: ast.ImportFrom) -> ast.ImportFrom:
221
+ """Rewrite ``from vclient.x import Y`` paths and rename imported names."""
222
+ if node.module and node.module in IMPORT_REWRITES:
223
+ node.module = IMPORT_REWRITES[node.module]
224
+
225
+ if node.names:
226
+ for alias in node.names:
227
+ if alias.name == "AsyncIterator":
228
+ alias.name = "Iterator"
229
+ elif alias.name in _ALL_RENAMES:
230
+ alias.name = _ALL_RENAMES[alias.name]
231
+
232
+ self.generic_visit(node)
233
+ return node
234
+
235
+ def visit_Import(self, node: ast.Import) -> ast.Import:
236
+ """Replace ``import asyncio`` with ``import time``."""
237
+ for alias in node.names:
238
+ if alias.name == "asyncio":
239
+ alias.name = "time"
240
+ return node
241
+
242
+ # ------------------------------------------------------------------
243
+ # Call rewriting
244
+ # ------------------------------------------------------------------
245
+
246
+ def visit_Call(self, node: ast.Call) -> ast.Call:
247
+ """Replace ``asyncio.sleep()`` with ``time.sleep()``."""
248
+ self.generic_visit(node)
249
+ if (
250
+ isinstance(node.func, ast.Attribute)
251
+ and isinstance(node.func.value, ast.Name)
252
+ and node.func.value.id == "asyncio"
253
+ and node.func.attr == "sleep"
254
+ ):
255
+ node.func.value.id = "time"
256
+ return node
257
+
258
+
259
+ # ---------------------------------------------------------------------------
260
+ # File-level helpers
261
+ # ---------------------------------------------------------------------------
262
+
263
+
264
+ def transform_file(source_path: Path) -> str:
265
+ """Read an async source file, transform it, and return the sync version.
266
+
267
+ Args:
268
+ source_path: Path to the async Python source file.
269
+
270
+ Returns:
271
+ The transformed synchronous source code with a generated header.
272
+ """
273
+ source = source_path.read_text()
274
+ tree = ast.parse(source)
275
+ transformer = AsyncToSyncTransformer()
276
+ new_tree = transformer.visit(tree)
277
+ ast.fix_missing_locations(new_tree)
278
+ return HEADER_COMMENT + ast.unparse(new_tree) + "\n"
279
+
280
+
281
+ def _write_sync_init(path: Path) -> None:
282
+ """Write the ``_sync/__init__.py`` that re-exports public names.
283
+
284
+ Args:
285
+ path: Path to the ``_sync/__init__.py`` file to write.
286
+ """
287
+ lines = [
288
+ HEADER_COMMENT,
289
+ "from vclient._sync.client import SyncVClient",
290
+ "from vclient._sync.registry import (",
291
+ " sync_books_service,",
292
+ " sync_campaigns_service,",
293
+ " sync_chapters_service,",
294
+ " sync_characters_service,",
295
+ " sync_character_autogen_service,",
296
+ " sync_character_blueprint_service,",
297
+ " sync_character_traits_service,",
298
+ " sync_clear_default_client,",
299
+ " sync_companies_service,",
300
+ " sync_configure_default_client,",
301
+ " sync_default_client,",
302
+ " sync_developer_service,",
303
+ " sync_dicerolls_service,",
304
+ " sync_dictionary_service,",
305
+ " sync_global_admin_service,",
306
+ " sync_options_service,",
307
+ " sync_system_service,",
308
+ " sync_users_service,",
309
+ ")",
310
+ "",
311
+ "__all__ = [",
312
+ ' "SyncVClient",',
313
+ ' "sync_books_service",',
314
+ ' "sync_campaigns_service",',
315
+ ' "sync_chapters_service",',
316
+ ' "sync_characters_service",',
317
+ ' "sync_character_autogen_service",',
318
+ ' "sync_character_blueprint_service",',
319
+ ' "sync_character_traits_service",',
320
+ ' "sync_clear_default_client",',
321
+ ' "sync_companies_service",',
322
+ ' "sync_configure_default_client",',
323
+ ' "sync_default_client",',
324
+ ' "sync_developer_service",',
325
+ ' "sync_dicerolls_service",',
326
+ ' "sync_dictionary_service",',
327
+ ' "sync_global_admin_service",',
328
+ ' "sync_options_service",',
329
+ ' "sync_system_service",',
330
+ ' "sync_users_service",',
331
+ "]",
332
+ "",
333
+ ]
334
+ path.write_text("\n".join(lines))
335
+
336
+
337
+ def generate_sync(src_dir: Path) -> None:
338
+ """Transform all async source files and write them into the ``_sync/`` package.
339
+
340
+ Args:
341
+ src_dir: Path to the ``src/vclient`` directory.
342
+ """
343
+ sync_dir = src_dir / "_sync"
344
+ sync_dir.mkdir(exist_ok=True)
345
+
346
+ # Transform top-level modules
347
+ for source_file in ["client.py", "registry.py"]:
348
+ source_path = src_dir / source_file
349
+ if source_path.exists():
350
+ output_path = sync_dir / source_file
351
+ output_path.write_text(transform_file(source_path))
352
+
353
+ # Transform services
354
+ services_src = src_dir / "services"
355
+ services_dst = sync_dir / "services"
356
+ services_dst.mkdir(exist_ok=True)
357
+
358
+ for source_path in sorted(services_src.glob("*.py")):
359
+ output_path = services_dst / source_path.name
360
+ output_path.write_text(transform_file(source_path))
361
+
362
+ # Write the _sync/__init__.py
363
+ _write_sync_init(sync_dir / "__init__.py")
364
+
365
+
366
+ if __name__ == "__main__":
367
+ project_root = Path(sys.argv[1]) if len(sys.argv) > 1 else Path("src/vclient")
368
+ generate_sync(project_root)
@@ -0,0 +1,45 @@
1
+ # AUTO-GENERATED — do not edit. Run 'uv run duty generate_sync' to regenerate.
2
+
3
+ from vclient._sync.client import SyncVClient
4
+ from vclient._sync.registry import (
5
+ sync_books_service,
6
+ sync_campaigns_service,
7
+ sync_chapters_service,
8
+ sync_character_autogen_service,
9
+ sync_character_blueprint_service,
10
+ sync_character_traits_service,
11
+ sync_characters_service,
12
+ sync_clear_default_client,
13
+ sync_companies_service,
14
+ sync_configure_default_client,
15
+ sync_default_client,
16
+ sync_developer_service,
17
+ sync_dicerolls_service,
18
+ sync_dictionary_service,
19
+ sync_global_admin_service,
20
+ sync_options_service,
21
+ sync_system_service,
22
+ sync_users_service,
23
+ )
24
+
25
+ __all__ = [
26
+ "SyncVClient",
27
+ "sync_books_service",
28
+ "sync_campaigns_service",
29
+ "sync_chapters_service",
30
+ "sync_character_autogen_service",
31
+ "sync_character_blueprint_service",
32
+ "sync_character_traits_service",
33
+ "sync_characters_service",
34
+ "sync_clear_default_client",
35
+ "sync_companies_service",
36
+ "sync_configure_default_client",
37
+ "sync_default_client",
38
+ "sync_developer_service",
39
+ "sync_dicerolls_service",
40
+ "sync_dictionary_service",
41
+ "sync_global_admin_service",
42
+ "sync_options_service",
43
+ "sync_system_service",
44
+ "sync_users_service",
45
+ ]