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,55 @@
1
+ [tool.poetry]
2
+ name = "{{ package_name }}"
3
+ version = "{{ version }}"
4
+ description = "{{ description }}"
5
+ authors = {{ authors | tojson }}
6
+ license = "{{ license }}"
7
+ readme = "README.md"
8
+ {% if repository_url %}repository = "{{ repository_url }}"
9
+ {% endif %}keywords = {{ keywords | tojson }}
10
+ classifiers = [
11
+ "Development Status :: 5 - Production/Stable",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Typing :: Typed",
17
+ ]
18
+
19
+ [tool.poetry.dependencies]
20
+ python = "{{ python_version }}"
21
+ pydantic = "^2.12"
22
+ httpx = "^0.28"
23
+ tenacity = "^9.1"
24
+ rich = "^14.1.0"
25
+
26
+ [tool.poetry.group.dev.dependencies]
27
+ pytest = "^8.0"
28
+ pytest-asyncio = "^0.24"
29
+ pytest-cov = "^6.0"
30
+ mypy = "^1.18"
31
+ ruff = "^0.13"
32
+
33
+ [build-system]
34
+ requires = ["poetry-core"]
35
+ build-backend = "poetry.core.masonry.api"
36
+
37
+ [tool.mypy]
38
+ python_version = "3.12"
39
+ strict = true
40
+ warn_return_any = true
41
+ warn_unused_configs = true
42
+ disallow_untyped_defs = true
43
+
44
+ [tool.ruff]
45
+ line-length = 100
46
+ target-version = "py312"
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E", "F", "I", "N", "UP", "B"]
50
+ ignore = []
51
+
52
+ [tool.pytest.ini_options]
53
+ asyncio_mode = "auto"
54
+ testpaths = ["tests"]
55
+ addopts = "--cov={{ package_name | replace('-', '_') }} --cov-report=html --cov-report=term"
@@ -0,0 +1,271 @@
1
+ """
2
+ Retry Configuration and Utilities
3
+
4
+ Provides automatic retry logic for failed HTTP requests using tenacity.
5
+ Retries only on network errors and server errors (5xx), not client errors (4xx).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Callable, Any
12
+ import httpx
13
+ from tenacity import (
14
+ retry,
15
+ stop_after_attempt,
16
+ wait_exponential,
17
+ retry_if_exception,
18
+ RetryCallState,
19
+ before_sleep_log,
20
+ )
21
+ import logging
22
+
23
+
24
+ @dataclass
25
+ class RetryConfig:
26
+ """
27
+ Retry configuration options.
28
+
29
+ Uses exponential backoff with jitter by default to avoid thundering herd.
30
+ """
31
+
32
+ max_attempts: int = 3
33
+ """Maximum number of retry attempts (default: 3)"""
34
+
35
+ min_wait: float = 1.0
36
+ """Minimum wait time between retries in seconds (default: 1.0)"""
37
+
38
+ max_wait: float = 60.0
39
+ """Maximum wait time between retries in seconds (default: 60.0)"""
40
+
41
+ multiplier: float = 2.0
42
+ """Exponential backoff multiplier (default: 2.0)"""
43
+
44
+ on_retry: Callable[[RetryCallState], None] | None = None
45
+ """Callback called on each retry attempt"""
46
+
47
+ logger: logging.Logger | None = None
48
+ """Logger for retry attempts (default: None)"""
49
+
50
+
51
+ DEFAULT_RETRY_CONFIG = RetryConfig()
52
+ """Default retry configuration"""
53
+
54
+
55
+ def should_retry(exception: BaseException) -> bool:
56
+ """
57
+ Determine if an error should trigger a retry.
58
+
59
+ Retries on:
60
+ - Network errors (connection refused, timeout, etc.)
61
+ - Server errors (5xx status codes)
62
+ - Rate limiting (429 status code)
63
+
64
+ Does NOT retry on:
65
+ - Client errors (4xx except 429)
66
+ - Authentication errors (401, 403)
67
+ - Not found (404)
68
+
69
+ Args:
70
+ exception: The exception to check
71
+
72
+ Returns:
73
+ True if should retry, False otherwise
74
+ """
75
+ # Always retry network errors
76
+ if isinstance(exception, (
77
+ httpx.NetworkError,
78
+ httpx.TimeoutException,
79
+ httpx.ConnectError,
80
+ httpx.ReadError,
81
+ httpx.WriteError,
82
+ httpx.PoolTimeout,
83
+ )):
84
+ return True
85
+
86
+ # For HTTP errors, check status code
87
+ if isinstance(exception, httpx.HTTPStatusError):
88
+ status = exception.response.status_code
89
+
90
+ # Retry on 5xx server errors
91
+ if 500 <= status < 600:
92
+ return True
93
+
94
+ # Retry on 429 (rate limit)
95
+ if status == 429:
96
+ return True
97
+
98
+ # Do NOT retry on 4xx client errors
99
+ return False
100
+
101
+ # Don't retry on unknown errors
102
+ return False
103
+
104
+
105
+ def create_retry_decorator(config: RetryConfig | None = None):
106
+ """
107
+ Create a retry decorator with the given configuration.
108
+
109
+ Args:
110
+ config: Retry configuration (uses defaults if None)
111
+
112
+ Returns:
113
+ Tenacity retry decorator
114
+
115
+ Example:
116
+ >>> retry_decorator = create_retry_decorator(RetryConfig(max_attempts=5))
117
+ >>> @retry_decorator
118
+ ... async def fetch_data():
119
+ ... async with httpx.AsyncClient() as client:
120
+ ... response = await client.get('https://api.example.com/users')
121
+ ... response.raise_for_status()
122
+ ... return response.json()
123
+ """
124
+ cfg = config or DEFAULT_RETRY_CONFIG
125
+
126
+ # Build retry decorator
127
+ retry_args = {
128
+ 'stop': stop_after_attempt(cfg.max_attempts),
129
+ 'wait': wait_exponential(
130
+ multiplier=cfg.multiplier,
131
+ min=cfg.min_wait,
132
+ max=cfg.max_wait,
133
+ ),
134
+ 'retry': retry_if_exception(should_retry),
135
+ 'reraise': True,
136
+ }
137
+
138
+ # Add logger if provided
139
+ if cfg.logger:
140
+ retry_args['before_sleep'] = before_sleep_log(cfg.logger, logging.WARNING)
141
+
142
+ # Add custom callback if provided
143
+ if cfg.on_retry:
144
+ original_before_sleep = retry_args.get('before_sleep')
145
+
146
+ def combined_before_sleep(retry_state: RetryCallState):
147
+ if original_before_sleep:
148
+ original_before_sleep(retry_state)
149
+ if cfg.on_retry:
150
+ cfg.on_retry(retry_state)
151
+
152
+ retry_args['before_sleep'] = combined_before_sleep
153
+
154
+ return retry(**retry_args)
155
+
156
+
157
+ async def with_retry(
158
+ fn: Callable[..., Any],
159
+ config: RetryConfig | None = None,
160
+ *args,
161
+ **kwargs
162
+ ) -> Any:
163
+ """
164
+ Execute an async function with retry logic.
165
+
166
+ Args:
167
+ fn: Async function to retry
168
+ config: Retry configuration (uses defaults if None)
169
+ *args: Positional arguments for fn
170
+ **kwargs: Keyword arguments for fn
171
+
172
+ Returns:
173
+ Result of the function
174
+
175
+ Example:
176
+ >>> async def fetch_users():
177
+ ... async with httpx.AsyncClient() as client:
178
+ ... response = await client.get('https://api.example.com/users')
179
+ ... response.raise_for_status()
180
+ ... return response.json()
181
+ >>>
182
+ >>> result = await with_retry(fetch_users, RetryConfig(max_attempts=5))
183
+ """
184
+ retry_decorator = create_retry_decorator(config)
185
+ retryable_fn = retry_decorator(fn)
186
+ return await retryable_fn(*args, **kwargs)
187
+
188
+
189
+ class RetryAsyncClient:
190
+ """
191
+ HTTP client wrapper that adds automatic retry logic.
192
+
193
+ Wraps httpx.AsyncClient and applies retry logic to all HTTP methods.
194
+ Transparently retries on network errors, 5xx status codes, and 429 rate limits.
195
+
196
+ Example:
197
+ >>> async with RetryAsyncClient('https://api.example.com', retry_config=RetryConfig(max_attempts=5)) as client:
198
+ ... response = await client.get('/users')
199
+ ... response.raise_for_status()
200
+ """
201
+
202
+ def __init__(
203
+ self,
204
+ base_url: str | None = None,
205
+ retry_config: RetryConfig | None = None,
206
+ **kwargs: Any
207
+ ):
208
+ """
209
+ Initialize retry-enabled HTTP client.
210
+
211
+ Args:
212
+ base_url: Base URL for all requests
213
+ retry_config: Retry configuration (None to disable retry)
214
+ **kwargs: Additional httpx.AsyncClient kwargs
215
+ """
216
+ self._client = httpx.AsyncClient(base_url=base_url, **kwargs)
217
+ self.retry_config = retry_config
218
+ self._retry_decorator = create_retry_decorator(retry_config) if retry_config else None
219
+
220
+ async def __aenter__(self) -> 'RetryAsyncClient':
221
+ await self._client.__aenter__()
222
+ return self
223
+
224
+ async def __aexit__(self, *args: Any) -> None:
225
+ await self._client.__aexit__(*args)
226
+
227
+ async def aclose(self) -> None:
228
+ """Close the HTTP client."""
229
+ await self._client.aclose()
230
+
231
+ def _wrap_with_retry(self, method: str):
232
+ """Wrap HTTP method with retry logic."""
233
+ original_method = getattr(self._client, method)
234
+
235
+ if self._retry_decorator:
236
+ async def wrapped(*args, **kwargs):
237
+ @self._retry_decorator
238
+ async def _do_request():
239
+ return await original_method(*args, **kwargs)
240
+ return await _do_request()
241
+ return wrapped
242
+ else:
243
+ return original_method
244
+
245
+ async def get(self, *args, **kwargs) -> httpx.Response:
246
+ """GET request with retry."""
247
+ return await self._wrap_with_retry('get')(*args, **kwargs)
248
+
249
+ async def post(self, *args, **kwargs) -> httpx.Response:
250
+ """POST request with retry."""
251
+ return await self._wrap_with_retry('post')(*args, **kwargs)
252
+
253
+ async def put(self, *args, **kwargs) -> httpx.Response:
254
+ """PUT request with retry."""
255
+ return await self._wrap_with_retry('put')(*args, **kwargs)
256
+
257
+ async def patch(self, *args, **kwargs) -> httpx.Response:
258
+ """PATCH request with retry."""
259
+ return await self._wrap_with_retry('patch')(*args, **kwargs)
260
+
261
+ async def delete(self, *args, **kwargs) -> httpx.Response:
262
+ """DELETE request with retry."""
263
+ return await self._wrap_with_retry('delete')(*args, **kwargs)
264
+
265
+ async def head(self, *args, **kwargs) -> httpx.Response:
266
+ """HEAD request with retry."""
267
+ return await self._wrap_with_retry('head')(*args, **kwargs)
268
+
269
+ async def options(self, *args, **kwargs) -> httpx.Response:
270
+ """OPTIONS request with retry."""
271
+ return await self._wrap_with_retry('options')(*args, **kwargs)
@@ -0,0 +1,14 @@
1
+ """
2
+ TypeScript Generator - Generates TypeScript client (Fetch API).
3
+
4
+ This generator creates a complete TypeScript API client from IR:
5
+ - TypeScript interfaces (Request/Response/Patch splits)
6
+ - Enum types from x-enum-varnames
7
+ - Fetch API for HTTP
8
+ - Django CSRF/session handling
9
+ - Type-safe
10
+ """
11
+
12
+ from .generator import TypeScriptGenerator
13
+
14
+ __all__ = ['TypeScriptGenerator']
@@ -0,0 +1,165 @@
1
+ """
2
+ TypeScript Client Generator - Generates TypeScript APIClient classes.
3
+ """
4
+
5
+ from __future__ import annotations
6
+
7
+ from jinja2 import Environment
8
+ from ...ir import IROperationObject
9
+ from ..base import GeneratedFile
10
+
11
+ class ClientGenerator:
12
+ """Generates TypeScript APIClient classes (flat and namespaced)."""
13
+
14
+ def __init__(self, jinja_env: Environment, context, base, operations_gen):
15
+ self.jinja_env = jinja_env
16
+ self.context = context
17
+ self.base = base
18
+ self.operations_gen = operations_gen
19
+
20
+ def generate_client_file(self):
21
+ """Generate client.ts with APIClient class."""
22
+
23
+
24
+ # Client class
25
+ client_code = self._generate_client_class()
26
+
27
+ template = self.jinja_env.get_template('client_file.ts.jinja')
28
+ content = template.render(
29
+ has_enums=bool(self.base.get_enum_schemas()),
30
+ client_code=client_code
31
+ )
32
+
33
+ return GeneratedFile(
34
+ path="client.ts",
35
+ content=content,
36
+ description="APIClient with HTTP adapter and error handling",
37
+ )
38
+
39
+ def _generate_client_class(self) -> str:
40
+ """Generate APIClient class."""
41
+ if self.base.client_structure == "namespaced":
42
+ return self._generate_namespaced_client()
43
+ else:
44
+ return self._generate_flat_client()
45
+
46
+ def _generate_flat_client(self) -> str:
47
+ """Generate flat APIClient (all methods in one class)."""
48
+ # Generate all operation methods
49
+ method_codes = []
50
+ for op_id, operation in self.context.operations.items():
51
+ method_codes.append(self.operations_gen.generate_operation(operation))
52
+
53
+ template = self.jinja_env.get_template('client/flat_client.ts.jinja')
54
+ return template.render(
55
+ api_title=self.context.openapi_info.title,
56
+ operations=method_codes
57
+ )
58
+
59
+ def _generate_namespaced_client(self) -> str:
60
+ """Generate namespaced APIClient (sub-clients per tag)."""
61
+ # Group operations by tag (using base class method)
62
+ ops_by_tag = self.base.group_operations_by_tag()
63
+
64
+ # Generate sub-client classes
65
+ sub_client_classes = []
66
+ for tag, operations in sorted(ops_by_tag.items()):
67
+ sub_client_classes.append(self._generate_sub_client_class(tag, operations))
68
+
69
+ sub_clients_code = "\n\n".join(sub_client_classes)
70
+
71
+ # Generate main APIClient
72
+ main_client_code = self._generate_main_client_class(list(ops_by_tag.keys()))
73
+
74
+ return f"{sub_clients_code}\n\n{main_client_code}"
75
+
76
+ def _generate_sub_client_class(self, tag: str, operations: list) -> str:
77
+ """Generate sub-client class for a specific tag."""
78
+ class_name = self.base.tag_to_class_name(tag)
79
+
80
+ # Generate methods for this tag
81
+ method_codes = []
82
+ for operation in operations:
83
+ method_codes.append(self.operations_gen.generate_operation(operation, remove_tag_prefix=True, in_subclient=True))
84
+
85
+ template = self.jinja_env.get_template('client/sub_client.ts.jinja')
86
+ return template.render(
87
+ tag=self.base.tag_to_display_name(tag),
88
+ class_name=class_name,
89
+ operations=method_codes
90
+ )
91
+
92
+ def _generate_main_client_class(self, ops_by_tag: dict) -> str:
93
+ """Generate main APIClient with sub-clients."""
94
+ tags = sorted(ops_by_tag.keys())
95
+
96
+ # Prepare data for template
97
+ tags_data = [
98
+ {
99
+ "class_name": self.base.tag_to_class_name(tag),
100
+ "property": self.base.tag_to_property_name(tag),
101
+ "slug": self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
102
+ }
103
+ for tag in tags
104
+ ]
105
+
106
+ template = self.jinja_env.get_template('client/client.ts.jinja')
107
+ return template.render(
108
+ sub_clients=True,
109
+ include_imports=False, # Imports already in main_client_file.ts.jinja
110
+ tags=tags_data,
111
+ info={"title": self.context.openapi_info.title},
112
+ )
113
+
114
+ def generate_main_client_file(self, ops_by_tag: dict):
115
+ """Generate main client.ts with APIClient."""
116
+
117
+ tags = sorted(ops_by_tag.keys())
118
+
119
+ # Prepare tags data for template
120
+ tags_data = [
121
+ {
122
+ "class_name": self.base.tag_to_class_name(tag),
123
+ "slug": self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag]),
124
+ }
125
+ for tag in tags
126
+ ]
127
+
128
+ # Generate main APIClient class
129
+ client_code = self._generate_main_client_class(ops_by_tag)
130
+
131
+ template = self.jinja_env.get_template('client/main_client_file.ts.jinja')
132
+ content = template.render(
133
+ tags=tags_data,
134
+ client_code=client_code
135
+ )
136
+
137
+ return GeneratedFile(
138
+ path="client.ts",
139
+ content=content,
140
+ description="Main API client with HTTP adapter and error handling",
141
+ )
142
+
143
+ def generate_app_client_file(self, tag: str, operations: list[IROperationObject]):
144
+ """Generate client.ts for a specific app."""
145
+
146
+ class_name = self.base.tag_to_class_name(tag)
147
+
148
+ # Generate methods
149
+ method_codes = []
150
+ for operation in operations:
151
+ method_codes.append(self.operations_gen.generate_operation(operation, remove_tag_prefix=True, in_subclient=True))
152
+
153
+ template = self.jinja_env.get_template('client/app_client.ts.jinja')
154
+ content = template.render(
155
+ tag=self.base.tag_to_display_name(tag),
156
+ class_name=class_name,
157
+ operations=method_codes
158
+ )
159
+
160
+ folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
161
+ return GeneratedFile(
162
+ path=f"{folder_name}/client.ts",
163
+ content=content,
164
+ description=f"API client for {tag}",
165
+ )