django-cfg 1.4.11__py3-none-any.whl → 1.4.13__py3-none-any.whl

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 (101) hide show
  1. django_cfg/core/generation/integration_generators/api.py +2 -1
  2. django_cfg/models/django/openapi.py +4 -80
  3. django_cfg/modules/django_client/core/archive/manager.py +2 -2
  4. django_cfg/modules/django_client/core/config/config.py +20 -0
  5. django_cfg/modules/django_client/core/config/service.py +1 -1
  6. django_cfg/modules/django_client/core/generator/__init__.py +4 -4
  7. django_cfg/modules/django_client/core/generator/base.py +71 -0
  8. django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
  9. django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
  10. django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
  11. django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
  12. django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
  13. django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
  14. django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
  15. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/api_wrapper.py.jinja +25 -2
  16. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client.py.jinja +24 -6
  17. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client_file.py.jinja +1 -0
  18. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/operation_method.py.jinja +3 -1
  19. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/sub_client.py.jinja +8 -1
  20. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
  21. django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
  22. django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
  23. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/main_init.py.jinja +2 -0
  24. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enum_class.py.jinja +3 -1
  25. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/schema_class.py.jinja +3 -1
  26. django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
  27. django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
  28. django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
  29. django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
  30. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
  31. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
  32. django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
  33. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
  34. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
  35. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
  36. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
  37. django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
  38. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/app_client.ts.jinja +1 -1
  39. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/client.ts.jinja +77 -1
  40. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/main_client_file.ts.jinja +1 -0
  41. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/sub_client.ts.jinja +3 -3
  42. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
  43. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
  44. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/main_index.ts.jinja +73 -11
  45. django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
  46. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
  47. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
  48. django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
  49. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/errors.ts.jinja +3 -1
  50. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/logger.ts.jinja +9 -1
  51. django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
  52. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/storage.ts.jinja +54 -10
  53. django_cfg/modules/django_client/management/commands/generate_client.py +5 -0
  54. django_cfg/modules/django_client/pytest.ini +30 -0
  55. django_cfg/modules/django_client/spectacular/__init__.py +3 -2
  56. django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
  57. django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
  58. django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
  59. django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
  60. django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
  61. django_cfg/modules/django_unfold/callbacks/main.py +6 -6
  62. django_cfg/pyproject.toml +1 -1
  63. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/METADATA +1 -1
  64. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/RECORD +99 -70
  65. django_cfg/modules/django_client/core/generator/python.py +0 -751
  66. django_cfg/modules/django_client/core/generator/typescript.py +0 -872
  67. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/__init__.py.jinja +0 -0
  68. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/app_init.py.jinja +0 -0
  69. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/app_client.py.jinja +0 -0
  70. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/flat_client.py.jinja +0 -0
  71. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client_file.py.jinja +0 -0
  72. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/app_models.py.jinja +0 -0
  73. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enums.py.jinja +0 -0
  74. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/models.py.jinja +0 -0
  75. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/logger.py.jinja +0 -0
  76. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/schema.py.jinja +0 -0
  77. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/app_index.ts.jinja +0 -0
  78. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/flat_client.ts.jinja +0 -0
  79. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/operation.ts.jinja +0 -0
  80. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client_file.ts.jinja +0 -0
  81. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/index.ts.jinja +0 -0
  82. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/app_models.ts.jinja +0 -0
  83. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/enums.ts.jinja +0 -0
  84. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/models.ts.jinja +0 -0
  85. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/http.ts.jinja +0 -0
  86. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/schema.ts.jinja +0 -0
  87. /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
  88. /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
  89. /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
  90. /django_cfg/{dashboard → modules/django_dashboard}/management/__init__.py +0 -0
  91. /django_cfg/{dashboard → modules/django_dashboard}/management/commands/__init__.py +0 -0
  92. /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
  93. /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
  94. /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
  95. /django_cfg/{dashboard → modules/django_dashboard}/sections/documentation.py +0 -0
  96. /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
  97. /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
  98. /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
  99. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
  100. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
  101. {django_cfg-1.4.11.dist-info → django_cfg-1.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,278 @@
1
+ """
2
+ Operations Generator - Generates operation methods for async and sync clients.
3
+
4
+ Handles:
5
+ - Async operation methods (async def with await)
6
+ - Sync operation methods (def without await)
7
+ - Path parameters, query parameters, request bodies
8
+ - Response parsing and validation
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from jinja2 import Environment
14
+
15
+ from ...ir import IROperationObject
16
+
17
+
18
+ class OperationsGenerator:
19
+ """Generates operation methods for Python clients."""
20
+
21
+ def __init__(self, jinja_env: Environment, base_generator):
22
+ """
23
+ Initialize operations generator.
24
+
25
+ Args:
26
+ jinja_env: Jinja2 environment for templates
27
+ base_generator: Reference to base generator for utility methods
28
+ """
29
+ self.jinja_env = jinja_env
30
+ self.base = base_generator
31
+
32
+ def generate_async_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
33
+ """Generate async method for operation."""
34
+ # Get method name
35
+ method_name = operation.operation_id
36
+ if remove_tag_prefix and operation.tags:
37
+ # Remove tag prefix using base class method
38
+ tag = operation.tags[0]
39
+ method_name = self.base.remove_tag_prefix(method_name, tag)
40
+
41
+ # Method signature
42
+ params = ["self"]
43
+
44
+ # Add path parameters
45
+ for param in operation.path_parameters:
46
+ param_type = self._map_param_type(param.schema_type)
47
+ params.append(f"{param.name}: {param_type}")
48
+
49
+ # Add request body parameter
50
+ if operation.request_body:
51
+ params.append(f"data: {operation.request_body.schema_name}")
52
+ elif operation.patch_request_body:
53
+ params.append(f"data: {operation.patch_request_body.schema_name} | None = None")
54
+
55
+ # Add query parameters
56
+ for param in operation.query_parameters:
57
+ param_type = self._map_param_type(param.schema_type)
58
+ if not param.required:
59
+ param_type = f"{param_type} | None = None"
60
+ params.append(f"{param.name}: {param_type}")
61
+
62
+ # Return type
63
+ primary_response = operation.primary_success_response
64
+ if primary_response and primary_response.schema_name:
65
+ if operation.is_list_operation:
66
+ return_type = f"list[{primary_response.schema_name}]"
67
+ else:
68
+ return_type = primary_response.schema_name
69
+ else:
70
+ return_type = "None"
71
+
72
+ signature = f"async def {method_name}({', '.join(params)}) -> {return_type}:"
73
+
74
+ # Docstring
75
+ docstring_lines = []
76
+ if operation.summary:
77
+ docstring_lines.append(operation.summary)
78
+ if operation.description:
79
+ if docstring_lines:
80
+ docstring_lines.append("")
81
+ docstring_lines.extend(self.base.wrap_comment(operation.description, 72))
82
+
83
+ docstring = "\n".join(docstring_lines) if docstring_lines else None
84
+
85
+ # Method body
86
+ body_lines = []
87
+
88
+ # Build URL
89
+ url_expr = f'"{operation.path}"'
90
+ if operation.path_parameters:
91
+ # Replace {id} with f-string {id}
92
+ url_expr = f'f"{operation.path}"'
93
+
94
+ body_lines.append(f"url = {url_expr}")
95
+
96
+ # Build request
97
+ request_kwargs = []
98
+
99
+ # Query params
100
+ if operation.query_parameters:
101
+ query_items = []
102
+ for param in operation.query_parameters:
103
+ if param.required:
104
+ query_items.append(f'"{param.name}": {param.name}')
105
+ else:
106
+ query_items.append(f'"{param.name}": {param.name} if {param.name} is not None else None')
107
+
108
+ query_dict = "{" + ", ".join(query_items) + "}"
109
+ request_kwargs.append(f"params={query_dict}")
110
+
111
+ # JSON body
112
+ if operation.request_body:
113
+ # Required body
114
+ request_kwargs.append("json=data.model_dump()")
115
+ elif operation.patch_request_body:
116
+ # Optional PATCH body - check for None
117
+ request_kwargs.append("json=data.model_dump() if data is not None else None")
118
+
119
+ # Make request
120
+ method_lower = operation.http_method.lower()
121
+ request_line = f"response = await self._client.{method_lower}(url"
122
+ if request_kwargs:
123
+ request_line += ", " + ", ".join(request_kwargs)
124
+ request_line += ")"
125
+
126
+ body_lines.append(request_line)
127
+
128
+ # Handle response
129
+ body_lines.append("response.raise_for_status()")
130
+
131
+ if return_type != "None":
132
+ if operation.is_list_operation:
133
+ # Paginated list response - extract results
134
+ body_lines.append(f"data = response.json()")
135
+ body_lines.append(f'return [{primary_response.schema_name}.model_validate(item) for item in data.get("results", [])]')
136
+ else:
137
+ body_lines.append(f"return {primary_response.schema_name}.model_validate(response.json())")
138
+ else:
139
+ body_lines.append("return None")
140
+
141
+ template = self.jinja_env.get_template('client/operation_method.py.jinja')
142
+ return template.render(
143
+ method_name=method_name,
144
+ params=params,
145
+ return_type=return_type,
146
+ docstring=docstring,
147
+ body_lines=body_lines
148
+ )
149
+
150
+ def generate_sync_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
151
+ """Generate sync method for operation (mirrors async generate_operation)."""
152
+ # Get method name
153
+ method_name = operation.operation_id
154
+ if remove_tag_prefix and operation.tags:
155
+ # Remove tag prefix using base class method
156
+ tag = operation.tags[0]
157
+ method_name = self.base.remove_tag_prefix(method_name, tag)
158
+
159
+ # Method signature
160
+ params = ["self"]
161
+
162
+ # Add path parameters
163
+ for param in operation.path_parameters:
164
+ param_type = self._map_param_type(param.schema_type)
165
+ params.append(f"{param.name}: {param_type}")
166
+
167
+ # Add request body parameter
168
+ if operation.request_body:
169
+ params.append(f"data: {operation.request_body.schema_name}")
170
+ elif operation.patch_request_body:
171
+ params.append(f"data: {operation.patch_request_body.schema_name} | None = None")
172
+
173
+ # Add query parameters
174
+ for param in operation.query_parameters:
175
+ param_type = self._map_param_type(param.schema_type)
176
+ if not param.required:
177
+ param_type = f"{param_type} | None = None"
178
+ params.append(f"{param.name}: {param_type}")
179
+
180
+ # Return type
181
+ primary_response = operation.primary_success_response
182
+ if primary_response and primary_response.schema_name:
183
+ if operation.is_list_operation:
184
+ return_type = f"list[{primary_response.schema_name}]"
185
+ else:
186
+ return_type = primary_response.schema_name
187
+ else:
188
+ return_type = "None"
189
+
190
+ # Docstring
191
+ docstring_lines = []
192
+ if operation.summary:
193
+ docstring_lines.append(operation.summary)
194
+ if operation.description:
195
+ if docstring_lines:
196
+ docstring_lines.append("")
197
+ docstring_lines.extend(self.base.wrap_comment(operation.description, 72))
198
+
199
+ docstring = "\n".join(docstring_lines) if docstring_lines else None
200
+
201
+ # Method body
202
+ body_lines = []
203
+
204
+ # Build URL
205
+ url_expr = f'"{operation.path}"'
206
+ if operation.path_parameters:
207
+ # Replace {id} with f-string {id}
208
+ url_expr = f'f"{operation.path}"'
209
+
210
+ body_lines.append(f"url = {url_expr}")
211
+
212
+ # Build request
213
+ request_kwargs = []
214
+
215
+ # Query params
216
+ if operation.query_parameters:
217
+ query_items = []
218
+ for param in operation.query_parameters:
219
+ if param.required:
220
+ query_items.append(f'"{param.name}": {param.name}')
221
+ else:
222
+ query_items.append(f'"{param.name}": {param.name} if {param.name} is not None else None')
223
+
224
+ query_dict = "{" + ", ".join(query_items) + "}"
225
+ request_kwargs.append(f"params={query_dict}")
226
+
227
+ # JSON body
228
+ if operation.request_body:
229
+ # Required body
230
+ request_kwargs.append("json=data.model_dump(exclude_unset=True)")
231
+ elif operation.patch_request_body:
232
+ # Optional PATCH body - check for None
233
+ request_kwargs.append("json=data.model_dump(exclude_unset=True) if data is not None else None")
234
+
235
+ # HTTP method
236
+ method_lower = operation.http_method.lower()
237
+
238
+ # Build request call (sync version - no await)
239
+ if request_kwargs:
240
+ request_call = f'self._client.{method_lower}(url, {", ".join(request_kwargs)})'
241
+ else:
242
+ request_call = f'self._client.{method_lower}(url)'
243
+
244
+ body_lines.append(f"response = {request_call}")
245
+ body_lines.append("response.raise_for_status()")
246
+
247
+ # Parse response
248
+ if return_type != "None":
249
+ if operation.is_list_operation:
250
+ # List response - validate each item (paginated)
251
+ primary_schema = primary_response.schema_name
252
+ body_lines.append(f"data = response.json()")
253
+ body_lines.append(f'return [{primary_schema}.model_validate(item) for item in data.get("results", [])]')
254
+ else:
255
+ # Single object response
256
+ body_lines.append(f"return {return_type}.model_validate(response.json())")
257
+
258
+ # Render template
259
+ template = self.jinja_env.get_template('client/sync_operation_method.py.jinja')
260
+ return template.render(
261
+ method_name=method_name,
262
+ params=params,
263
+ return_type=return_type,
264
+ body_lines=body_lines,
265
+ docstring=docstring
266
+ )
267
+
268
+ def _map_param_type(self, schema_type: str) -> str:
269
+ """Map parameter schema type to Python type."""
270
+ type_map = {
271
+ "string": "str",
272
+ "integer": "int",
273
+ "number": "float",
274
+ "boolean": "bool",
275
+ "array": "list",
276
+ "object": "dict",
277
+ }
278
+ return type_map.get(schema_type, "str")
@@ -0,0 +1,102 @@
1
+ """
2
+ Sync Client Generator - Generates sync Python clients.
3
+
4
+ Handles:
5
+ - Main SyncAPIClient class (httpx.Client)
6
+ - Sync sub-client classes (per tag/app)
7
+ - Mirrors async client structure without async/await
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from jinja2 import Environment
13
+
14
+ from ..base import GeneratedFile
15
+ from ...ir import IROperationObject
16
+
17
+
18
+ class SyncClientGenerator:
19
+ """Generates sync Python client files."""
20
+
21
+ def __init__(self, jinja_env: Environment, base_generator, operations_gen):
22
+ """
23
+ Initialize sync client generator.
24
+
25
+ Args:
26
+ jinja_env: Jinja2 environment for templates
27
+ base_generator: Reference to base generator
28
+ operations_gen: Operations generator instance
29
+ """
30
+ self.jinja_env = jinja_env
31
+ self.base = base_generator
32
+ self.operations_gen = operations_gen
33
+
34
+ def generate_app_sync_client_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
35
+ """Generate sync_client.py for a specific app."""
36
+ class_name = self.base.tag_to_class_name(tag)
37
+
38
+ # Generate sync methods
39
+ method_codes = []
40
+ for operation in operations:
41
+ method_codes.append(self.operations_gen.generate_sync_operation(operation, remove_tag_prefix=True))
42
+
43
+ template = self.jinja_env.get_template('client/sync_sub_client.py.jinja')
44
+ content = template.render(
45
+ tag=self.base.tag_to_display_name(tag),
46
+ class_name=class_name,
47
+ operations=method_codes
48
+ )
49
+
50
+ folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
51
+ return GeneratedFile(
52
+ path=f"{folder_name}/sync_client.py",
53
+ content=content,
54
+ description=f"Sync API client for {tag}",
55
+ )
56
+
57
+ def generate_sync_main_client_file(self, ops_by_tag: dict) -> GeneratedFile:
58
+ """Generate sync_client.py with SyncAPIClient."""
59
+ tags = sorted(ops_by_tag.keys())
60
+
61
+ # Prepare tags data for template (with Sync prefix for imports)
62
+ tags_data = [
63
+ {
64
+ "class_name": f"Sync{self.base.tag_to_class_name(tag)}", # Add Sync prefix
65
+ "slug": f"{self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag])}.sync_client", # Import from sync_client module
66
+ }
67
+ for tag in tags
68
+ ]
69
+
70
+ # Generate sync APIClient class
71
+ sync_client_code = self._generate_sync_main_client_class(ops_by_tag)
72
+
73
+ template = self.jinja_env.get_template('client/main_client_file.py.jinja')
74
+ content = template.render(
75
+ tags=tags_data,
76
+ client_code=sync_client_code
77
+ )
78
+
79
+ return GeneratedFile(
80
+ path="sync_client.py",
81
+ content=content,
82
+ description="Main sync API client",
83
+ )
84
+
85
+ def _generate_sync_main_client_class(self, ops_by_tag: dict) -> str:
86
+ """Generate main SyncAPIClient with sync sub-clients."""
87
+ tags = sorted(ops_by_tag.keys())
88
+
89
+ # Prepare tags data for template
90
+ tags_data = [
91
+ {
92
+ "class_name": self.base.tag_to_class_name(tag),
93
+ "property": self.base.tag_to_property_name(tag),
94
+ }
95
+ for tag in tags
96
+ ]
97
+
98
+ template = self.jinja_env.get_template('client/sync_main_client.py.jinja')
99
+ return template.render(
100
+ api_title=self.base.context.openapi_info.title,
101
+ tags=tags_data
102
+ )
@@ -6,24 +6,42 @@ class API:
6
6
  - Thread-safe JWT token storage
7
7
  - Automatic Authorization header injection
8
8
  - Context manager support for async operations
9
+ - Optional retry and logging configuration
9
10
 
10
11
  Example:
11
12
  >>> api = API('https://api.example.com')
12
13
  >>> api.set_token('jwt-token')
13
14
  >>> async with api:
14
15
  ... users = await api.users.list()
16
+ >>>
17
+ >>> # With retry and logging
18
+ >>> api = API(
19
+ ... 'https://api.example.com',
20
+ ... retry_config=RetryConfig(max_attempts=5),
21
+ ... logger_config=LoggerConfig(enabled=True)
22
+ ... )
15
23
  """
16
24
 
17
- def __init__(self, base_url: str, **kwargs: Any):
25
+ def __init__(
26
+ self,
27
+ base_url: str,
28
+ logger_config: LoggerConfig | None = None,
29
+ retry_config: RetryConfig | None = None,
30
+ **kwargs: Any
31
+ ):
18
32
  """
19
33
  Initialize API client.
20
34
 
21
35
  Args:
22
36
  base_url: Base API URL (e.g., 'https://api.example.com')
37
+ logger_config: Logger configuration (None to disable logging)
38
+ retry_config: Retry configuration (None to disable retry)
23
39
  **kwargs: Additional httpx.AsyncClient kwargs
24
40
  """
25
41
  self.base_url = base_url.rstrip('/')
26
42
  self._kwargs = kwargs
43
+ self._logger_config = logger_config
44
+ self._retry_config = retry_config
27
45
  self._token: str | None = None
28
46
  self._refresh_token: str | None = None
29
47
  self._lock = threading.Lock()
@@ -42,7 +60,12 @@ class API:
42
60
  kwargs['headers'] = headers
43
61
 
44
62
  # Create new APIClient
45
- self._client = APIClient(self.base_url, **kwargs)
63
+ self._client = APIClient(
64
+ self.base_url,
65
+ logger_config=self._logger_config,
66
+ retry_config=self._retry_config,
67
+ **kwargs
68
+ )
46
69
 
47
70
  {% for prop in properties %}
48
71
  @property
@@ -6,12 +6,18 @@ class APIClient:
6
6
  >>> async with APIClient(base_url='https://api.example.com') as client:
7
7
  ... users = await client.users.list()
8
8
  ... post = await client.posts.create(data=new_post)
9
+ >>>
10
+ >>> # With retry configuration
11
+ >>> retry_config = RetryConfig(max_attempts=5, min_wait=2.0)
12
+ >>> async with APIClient(base_url='https://api.example.com', retry_config=retry_config) as client:
13
+ ... users = await client.users.list()
9
14
  """
10
15
 
11
16
  def __init__(
12
17
  self,
13
18
  base_url: str,
14
19
  logger_config: Optional[LoggerConfig] = None,
20
+ retry_config: Optional[RetryConfig] = None,
15
21
  **kwargs: Any,
16
22
  ):
17
23
  """
@@ -20,14 +26,25 @@ class APIClient:
20
26
  Args:
21
27
  base_url: Base API URL (e.g., 'https://api.example.com')
22
28
  logger_config: Logger configuration (None to disable logging)
29
+ retry_config: Retry configuration (None to disable retry)
23
30
  **kwargs: Additional httpx.AsyncClient kwargs
24
31
  """
25
32
  self.base_url = base_url.rstrip('/')
26
- self._client = httpx.AsyncClient(
27
- base_url=self.base_url,
28
- timeout=30.0,
29
- **kwargs,
30
- )
33
+
34
+ # Create HTTP client with or without retry
35
+ if retry_config is not None:
36
+ self._client = RetryAsyncClient(
37
+ base_url=self.base_url,
38
+ retry_config=retry_config,
39
+ timeout=30.0,
40
+ **kwargs,
41
+ )
42
+ else:
43
+ self._client = httpx.AsyncClient(
44
+ base_url=self.base_url,
45
+ timeout=30.0,
46
+ **kwargs,
47
+ )
31
48
 
32
49
  # Initialize logger
33
50
  self.logger: Optional[APILogger] = None
@@ -40,10 +57,11 @@ class APIClient:
40
57
  {% endfor %}
41
58
 
42
59
  async def __aenter__(self) -> 'APIClient':
60
+ await self._client.__aenter__()
43
61
  return self
44
62
 
45
63
  async def __aexit__(self, *args: Any) -> None:
46
- await self._client.aclose()
64
+ await self._client.__aexit__(*args)
47
65
 
48
66
  async def close(self) -> None:
49
67
  """Close HTTP client."""
@@ -8,6 +8,7 @@ import httpx
8
8
  from .{{ tag.slug }} import {{ tag.class_name }}
9
9
  {% endfor %}
10
10
  from .logger import APILogger, LoggerConfig
11
+ from .retry import RetryConfig, RetryAsyncClient
11
12
 
12
13
 
13
14
  {{ client_code }}
@@ -1,6 +1,8 @@
1
1
  async def {{ method_name }}({{ params | join(', ') }}) -> {{ return_type }}:
2
2
  {% if docstring %}
3
- """{{ docstring }}"""
3
+ """
4
+ {{ docstring | indent(4, first=True) }}
5
+ """
4
6
  {% endif %}
5
7
  {% for line in body_lines %}
6
8
  {{ line }}
@@ -1,3 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from .models import *
6
+
7
+
1
8
  class {{ class_name }}:
2
9
  """API endpoints for {{ tag }}."""
3
10
 
@@ -6,6 +13,6 @@ class {{ class_name }}:
6
13
  self._client = client
7
14
 
8
15
  {% for operation in operations %}
9
- {{ operation }}
16
+ {{ operation | indent(4, first=True) }}
10
17
 
11
18
  {% endfor %}
@@ -0,0 +1,50 @@
1
+ class SyncAPIClient:
2
+ """
3
+ Synchronous API client for {{ api_title }}.
4
+
5
+ Usage:
6
+ >>> with SyncAPIClient(base_url='https://api.example.com') as client:
7
+ ... users = client.users.list()
8
+ ... post = client.posts.create(data=new_post)
9
+ """
10
+
11
+ def __init__(
12
+ self,
13
+ base_url: str,
14
+ logger_config: Optional[LoggerConfig] = None,
15
+ **kwargs: Any,
16
+ ):
17
+ """
18
+ Initialize sync API client.
19
+
20
+ Args:
21
+ base_url: Base API URL (e.g., 'https://api.example.com')
22
+ logger_config: Logger configuration (None to disable logging)
23
+ **kwargs: Additional httpx.Client kwargs
24
+ """
25
+ self.base_url = base_url.rstrip('/')
26
+ self._client = httpx.Client(
27
+ base_url=self.base_url,
28
+ timeout=30.0,
29
+ **kwargs,
30
+ )
31
+
32
+ # Initialize logger
33
+ self.logger: Optional[APILogger] = None
34
+ if logger_config is not None:
35
+ self.logger = APILogger(logger_config)
36
+
37
+ # Initialize sub-clients
38
+ {% for tag in tags %}
39
+ self.{{ tag.property }} = Sync{{ tag.class_name }}(self._client)
40
+ {% endfor %}
41
+
42
+ def __enter__(self) -> 'SyncAPIClient':
43
+ return self
44
+
45
+ def __exit__(self, *args: Any) -> None:
46
+ self._client.close()
47
+
48
+ def close(self) -> None:
49
+ """Close HTTP client."""
50
+ self._client.close()
@@ -0,0 +1,9 @@
1
+ def {{ method_name }}({{ params | join(', ') }}) -> {{ return_type }}:
2
+ {% if docstring %}
3
+ """
4
+ {{ docstring | indent(4, first=True) }}
5
+ """
6
+ {% endif %}
7
+ {% for line in body_lines %}
8
+ {{ line }}
9
+ {% endfor %}
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from .models import *
6
+
7
+
8
+ class Sync{{ class_name }}:
9
+ """Synchronous API endpoints for {{ tag }}."""
10
+
11
+ def __init__(self, client: httpx.Client):
12
+ """Initialize sync sub-client with shared httpx client."""
13
+ self._client = client
14
+
15
+ {% for operation in operations %}
16
+ {{ operation | indent(4, first=True) }}
17
+
18
+ {% endfor %}
@@ -31,6 +31,8 @@ import httpx
31
31
 
32
32
  from .client import APIClient
33
33
  from .schema import OPENAPI_SCHEMA
34
+ from .logger import LoggerConfig
35
+ from .retry import RetryConfig
34
36
  {% for tag in tags %}
35
37
  from .{{ tag.slug }} import {{ tag.class_name }}
36
38
  {% endfor %}
@@ -1,6 +1,8 @@
1
1
  class {{ name }}({{ base_class }}):
2
2
  {% if docstring %}
3
- """{{ docstring }}"""
3
+ """
4
+ {{ docstring | indent(4, first=True) }}
5
+ """
4
6
  {% endif %}
5
7
  {% if members %}
6
8
  {% if docstring %}
@@ -1,6 +1,8 @@
1
1
  class {{ name }}(BaseModel):
2
2
  {% if docstring %}
3
- """{{ docstring }}"""
3
+ """
4
+ {{ docstring | indent(4, first=True) }}
5
+ """
4
6
  {% endif %}
5
7
 
6
8
  model_config = ConfigDict(