django-cfg 1.4.11__py3-none-any.whl → 1.4.14__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.
- django_cfg/apps/urls.py +120 -108
- django_cfg/core/generation/integration_generators/api.py +2 -1
- django_cfg/core/integration/url_integration.py +5 -10
- django_cfg/models/django/openapi.py +15 -128
- django_cfg/modules/django_client/core/archive/manager.py +2 -2
- django_cfg/modules/django_client/core/config/config.py +20 -0
- django_cfg/modules/django_client/core/config/service.py +1 -1
- django_cfg/modules/django_client/core/generator/__init__.py +4 -4
- django_cfg/modules/django_client/core/generator/base.py +71 -0
- django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
- django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
- django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
- django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
- django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
- django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
- django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/api_wrapper.py.jinja +25 -2
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client.py.jinja +24 -6
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client_file.py.jinja +1 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/operation_method.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/sub_client.py.jinja +8 -1
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/main_init.py.jinja +2 -0
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enum_class.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/schema_class.py.jinja +3 -1
- django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
- django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
- django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +539 -0
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
- django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/app_client.ts.jinja +1 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/client.ts.jinja +77 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/main_client_file.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/sub_client.ts.jinja +3 -3
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/main_index.ts.jinja +73 -11
- django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/errors.ts.jinja +3 -1
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/logger.ts.jinja +9 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
- django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/storage.ts.jinja +54 -10
- django_cfg/modules/django_client/management/commands/generate_client.py +5 -0
- django_cfg/modules/django_client/pytest.ini +30 -0
- django_cfg/modules/django_client/spectacular/__init__.py +3 -2
- django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
- django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
- django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
- django_cfg/modules/django_unfold/callbacks/main.py +6 -6
- django_cfg/modules/django_unfold/dashboard.py +6 -6
- django_cfg/pyproject.toml +1 -1
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/METADATA +1 -1
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/RECORD +100 -78
- django_cfg/dashboard/DEBUG_README.md +0 -105
- django_cfg/dashboard/REFACTORING_SUMMARY.md +0 -237
- django_cfg/modules/django_client/core/generator/python.py +0 -751
- django_cfg/modules/django_client/core/generator/typescript.py +0 -872
- django_cfg/modules/django_drf_theme/CHANGELOG.md +0 -210
- django_cfg/modules/django_drf_theme/EXAMPLE.md +0 -465
- django_cfg/modules/django_drf_theme/IMPLEMENTATION.md +0 -232
- django_cfg/modules/django_drf_theme/README.md +0 -207
- django_cfg/modules/django_drf_theme/TAILWIND_CDN_GUIDE.md +0 -274
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/__init__.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/app_init.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/app_client.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/flat_client.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client_file.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/app_models.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enums.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/models.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/logger.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/schema.py.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/app_index.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/flat_client.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/operation.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client_file.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/index.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/app_models.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/enums.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/models.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/http.ts.jinja +0 -0
- /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/schema.ts.jinja +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/management/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/management/commands/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/documentation.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,329 @@
|
|
1
|
+
"""
|
2
|
+
Zod Schemas Generator - Generates Zod validation schemas from IR.
|
3
|
+
|
4
|
+
This generator creates Zod schemas for runtime validation:
|
5
|
+
- Object schemas (z.object)
|
6
|
+
- Enum schemas (z.nativeEnum)
|
7
|
+
- Array schemas (z.array)
|
8
|
+
- Type inference (z.infer<typeof Schema>)
|
9
|
+
"""
|
10
|
+
|
11
|
+
from __future__ import annotations
|
12
|
+
|
13
|
+
from jinja2 import Environment
|
14
|
+
from ..base import GeneratedFile, BaseGenerator
|
15
|
+
from ...ir import IRContext, IRSchemaObject
|
16
|
+
|
17
|
+
|
18
|
+
class SchemasGenerator:
|
19
|
+
"""
|
20
|
+
Generate Zod schemas from IR schemas.
|
21
|
+
|
22
|
+
Features:
|
23
|
+
- Runtime validation with Zod
|
24
|
+
- Type inference from schemas
|
25
|
+
- Enum validation with z.nativeEnum()
|
26
|
+
- Nested object validation
|
27
|
+
- Array and nullable types
|
28
|
+
"""
|
29
|
+
|
30
|
+
def __init__(self, jinja_env: Environment, context: IRContext, base: BaseGenerator):
|
31
|
+
self.jinja_env = jinja_env
|
32
|
+
self.context = context
|
33
|
+
self.base = base
|
34
|
+
|
35
|
+
def generate_schema(self, schema: IRSchemaObject) -> str:
|
36
|
+
"""
|
37
|
+
Generate Zod schema for a single IR schema.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
schema: IRSchemaObject to convert to Zod
|
41
|
+
|
42
|
+
Returns:
|
43
|
+
Zod schema code as string
|
44
|
+
|
45
|
+
Examples:
|
46
|
+
>>> generate_schema(User)
|
47
|
+
export const UserSchema = z.object({
|
48
|
+
id: z.number(),
|
49
|
+
email: z.string().email(),
|
50
|
+
username: z.string().min(1).max(150),
|
51
|
+
})
|
52
|
+
"""
|
53
|
+
if schema.type == "object":
|
54
|
+
return self._generate_object_schema(schema)
|
55
|
+
elif schema.type == "array":
|
56
|
+
return self._generate_array_schema(schema)
|
57
|
+
elif schema.enum:
|
58
|
+
return self._generate_enum_schema(schema)
|
59
|
+
else:
|
60
|
+
# Primitive type
|
61
|
+
return self._map_type_to_zod(schema)
|
62
|
+
|
63
|
+
def _generate_object_schema(self, schema: IRSchemaObject) -> str:
|
64
|
+
"""Generate z.object() schema."""
|
65
|
+
lines = []
|
66
|
+
|
67
|
+
# Schema comment
|
68
|
+
if schema.description:
|
69
|
+
lines.append(f"/**\n * {schema.description}\n */")
|
70
|
+
|
71
|
+
# Start schema definition
|
72
|
+
lines.append(f"export const {schema.name}Schema = z.object({{")
|
73
|
+
|
74
|
+
# Generate fields
|
75
|
+
if schema.properties:
|
76
|
+
for prop_name, prop_schema in schema.properties.items():
|
77
|
+
field_code = self._generate_field(prop_name, prop_schema, schema.required)
|
78
|
+
lines.append(f" {field_code},")
|
79
|
+
|
80
|
+
lines.append("})")
|
81
|
+
|
82
|
+
return "\n".join(lines)
|
83
|
+
|
84
|
+
def _generate_field(
|
85
|
+
self,
|
86
|
+
name: str,
|
87
|
+
schema: IRSchemaObject,
|
88
|
+
required_fields: list[str],
|
89
|
+
) -> str:
|
90
|
+
"""
|
91
|
+
Generate Zod field validation.
|
92
|
+
|
93
|
+
Examples:
|
94
|
+
id: z.number()
|
95
|
+
email: z.string().email()
|
96
|
+
username: z.string().min(1).max(150)
|
97
|
+
status: z.nativeEnum(Enums.StatusEnum)
|
98
|
+
created_at: z.string().datetime()
|
99
|
+
"""
|
100
|
+
# Check if this field is an enum
|
101
|
+
if schema.enum and schema.name:
|
102
|
+
# Use enum validation
|
103
|
+
zod_type = f"z.nativeEnum(Enums.{self.base.sanitize_enum_name(schema.name)})"
|
104
|
+
# Check if this field is a reference to an enum
|
105
|
+
elif schema.ref and schema.ref in self.context.schemas:
|
106
|
+
ref_schema = self.context.schemas[schema.ref]
|
107
|
+
if ref_schema.enum:
|
108
|
+
# Reference to enum component
|
109
|
+
zod_type = f"z.nativeEnum(Enums.{self.base.sanitize_enum_name(schema.ref)})"
|
110
|
+
else:
|
111
|
+
# Reference to another schema
|
112
|
+
zod_type = f"{schema.ref}Schema"
|
113
|
+
else:
|
114
|
+
# Map TypeScript type to Zod type
|
115
|
+
zod_type = self._map_type_to_zod(schema)
|
116
|
+
|
117
|
+
# Check if required
|
118
|
+
is_required = name in required_fields
|
119
|
+
|
120
|
+
# Handle optional fields
|
121
|
+
if not is_required:
|
122
|
+
zod_type = f"{zod_type}.optional()"
|
123
|
+
|
124
|
+
# Handle nullable fields
|
125
|
+
if schema.nullable:
|
126
|
+
zod_type = f"{zod_type}.nullable()"
|
127
|
+
|
128
|
+
return f"{name}: {zod_type}"
|
129
|
+
|
130
|
+
def _map_type_to_zod(self, schema: IRSchemaObject) -> str:
|
131
|
+
"""
|
132
|
+
Map OpenAPI/TypeScript type to Zod validation.
|
133
|
+
|
134
|
+
Args:
|
135
|
+
schema: IRSchemaObject with type information
|
136
|
+
|
137
|
+
Returns:
|
138
|
+
Zod validation code
|
139
|
+
|
140
|
+
Examples:
|
141
|
+
string -> z.string()
|
142
|
+
string (format: email) -> z.string().email()
|
143
|
+
string (format: date-time) -> z.string().datetime()
|
144
|
+
string (format: uri) -> z.string().url()
|
145
|
+
integer -> z.number().int()
|
146
|
+
number -> z.number()
|
147
|
+
boolean -> z.boolean()
|
148
|
+
array -> z.array(...)
|
149
|
+
"""
|
150
|
+
schema_type = schema.type
|
151
|
+
schema_format = schema.format
|
152
|
+
|
153
|
+
# String types with format validation
|
154
|
+
if schema_type == "string":
|
155
|
+
base_type = "z.string()"
|
156
|
+
|
157
|
+
# Add format validation
|
158
|
+
if schema_format == "email":
|
159
|
+
base_type = "z.string().email()"
|
160
|
+
elif schema_format in ("date-time", "datetime"):
|
161
|
+
base_type = "z.string().datetime()"
|
162
|
+
elif schema_format == "date":
|
163
|
+
base_type = "z.string().date()"
|
164
|
+
elif schema_format in ("uri", "url"):
|
165
|
+
base_type = "z.string().url()"
|
166
|
+
elif schema_format == "uuid":
|
167
|
+
base_type = "z.string().uuid()"
|
168
|
+
|
169
|
+
# Add length constraints
|
170
|
+
if schema.min_length is not None:
|
171
|
+
base_type = f"{base_type}.min({schema.min_length})"
|
172
|
+
if schema.max_length is not None:
|
173
|
+
base_type = f"{base_type}.max({schema.max_length})"
|
174
|
+
|
175
|
+
# Add pattern validation
|
176
|
+
if schema.pattern:
|
177
|
+
# Escape regex pattern for JS
|
178
|
+
escaped_pattern = schema.pattern.replace('\\', '\\\\')
|
179
|
+
base_type = f"{base_type}.regex(/{escaped_pattern}/)"
|
180
|
+
|
181
|
+
return base_type
|
182
|
+
|
183
|
+
# Integer type
|
184
|
+
elif schema_type == "integer":
|
185
|
+
base_type = "z.number().int()"
|
186
|
+
|
187
|
+
# Add range constraints
|
188
|
+
if schema.minimum is not None:
|
189
|
+
base_type = f"{base_type}.min({schema.minimum})"
|
190
|
+
if schema.maximum is not None:
|
191
|
+
base_type = f"{base_type}.max({schema.maximum})"
|
192
|
+
|
193
|
+
return base_type
|
194
|
+
|
195
|
+
# Number type
|
196
|
+
elif schema_type == "number":
|
197
|
+
base_type = "z.number()"
|
198
|
+
|
199
|
+
# Add range constraints
|
200
|
+
if schema.minimum is not None:
|
201
|
+
base_type = f"{base_type}.min({schema.minimum})"
|
202
|
+
if schema.maximum is not None:
|
203
|
+
base_type = f"{base_type}.max({schema.maximum})"
|
204
|
+
|
205
|
+
return base_type
|
206
|
+
|
207
|
+
# Boolean type
|
208
|
+
elif schema_type == "boolean":
|
209
|
+
return "z.boolean()"
|
210
|
+
|
211
|
+
# Array type
|
212
|
+
elif schema_type == "array":
|
213
|
+
if schema.items:
|
214
|
+
item_type = self._map_type_to_zod(schema.items)
|
215
|
+
return f"z.array({item_type})"
|
216
|
+
return "z.array(z.any())"
|
217
|
+
|
218
|
+
# Object type
|
219
|
+
elif schema_type == "object":
|
220
|
+
# Only reference schema if it's a defined component (not an inline property)
|
221
|
+
# Inline objects should use z.record() or z.object({})
|
222
|
+
if schema.ref:
|
223
|
+
# Explicit reference
|
224
|
+
return f"{schema.ref}Schema"
|
225
|
+
elif schema.properties:
|
226
|
+
# Inline object with properties - shouldn't reach here, but use z.object
|
227
|
+
return "z.object({})"
|
228
|
+
else:
|
229
|
+
# Object with no properties (like additionalProperties: {})
|
230
|
+
# Use z.record(z.string(), z.any()) for dynamic objects
|
231
|
+
return "z.record(z.string(), z.any())"
|
232
|
+
|
233
|
+
# Fallback to any
|
234
|
+
return "z.any()"
|
235
|
+
|
236
|
+
def _generate_array_schema(self, schema: IRSchemaObject) -> str:
|
237
|
+
"""Generate z.array() schema."""
|
238
|
+
lines = []
|
239
|
+
|
240
|
+
if schema.description:
|
241
|
+
lines.append(f"/**\n * {schema.description}\n */")
|
242
|
+
|
243
|
+
item_type = "z.any()"
|
244
|
+
if schema.items:
|
245
|
+
item_type = self._map_type_to_zod(schema.items)
|
246
|
+
|
247
|
+
lines.append(f"export const {schema.name}Schema = z.array({item_type})")
|
248
|
+
|
249
|
+
return "\n".join(lines)
|
250
|
+
|
251
|
+
def _generate_enum_schema(self, schema: IRSchemaObject) -> str:
|
252
|
+
"""Generate z.nativeEnum() schema."""
|
253
|
+
enum_name = self.base.sanitize_enum_name(schema.name)
|
254
|
+
|
255
|
+
lines = []
|
256
|
+
|
257
|
+
if schema.description:
|
258
|
+
lines.append(f"/**\n * {schema.description}\n */")
|
259
|
+
|
260
|
+
lines.append(f"export const {enum_name}Schema = z.nativeEnum(Enums.{enum_name})")
|
261
|
+
|
262
|
+
return "\n".join(lines)
|
263
|
+
|
264
|
+
def generate_schema_file(self, schema: IRSchemaObject, refs: set[str]) -> GeneratedFile:
|
265
|
+
"""
|
266
|
+
Generate individual Zod schema file.
|
267
|
+
|
268
|
+
Args:
|
269
|
+
schema: Schema to generate
|
270
|
+
refs: Set of schema names that are referenced
|
271
|
+
|
272
|
+
Returns:
|
273
|
+
GeneratedFile with Zod schema
|
274
|
+
"""
|
275
|
+
# Generate schema code
|
276
|
+
schema_code = self.generate_schema(schema)
|
277
|
+
|
278
|
+
# Check if has enums
|
279
|
+
has_enums = self._schema_uses_enums(schema)
|
280
|
+
|
281
|
+
# Render template
|
282
|
+
template = self.jinja_env.get_template("schemas/schema.ts.jinja")
|
283
|
+
content = template.render(
|
284
|
+
schema_name=schema.name,
|
285
|
+
description=schema.description,
|
286
|
+
schema_code=schema_code,
|
287
|
+
has_enums=has_enums,
|
288
|
+
has_refs=bool(refs),
|
289
|
+
refs=sorted(refs),
|
290
|
+
)
|
291
|
+
|
292
|
+
return GeneratedFile(
|
293
|
+
path=f"_utils/schemas/{schema.name}.schema.ts",
|
294
|
+
content=content,
|
295
|
+
description=f"Zod schema for {schema.name}",
|
296
|
+
)
|
297
|
+
|
298
|
+
def generate_schemas_index_file(self, schema_names: list[str]) -> GeneratedFile:
|
299
|
+
"""Generate index.ts for schemas folder."""
|
300
|
+
template = self.jinja_env.get_template("schemas/index.ts.jinja")
|
301
|
+
content = template.render(schema_names=sorted(schema_names))
|
302
|
+
|
303
|
+
return GeneratedFile(
|
304
|
+
path="_utils/schemas/index.ts",
|
305
|
+
content=content,
|
306
|
+
description="Zod schemas index",
|
307
|
+
)
|
308
|
+
|
309
|
+
def _schema_uses_enums(self, schema: IRSchemaObject) -> bool:
|
310
|
+
"""Check if schema uses any enums."""
|
311
|
+
if schema.enum:
|
312
|
+
return True
|
313
|
+
|
314
|
+
if schema.properties:
|
315
|
+
for prop in schema.properties.values():
|
316
|
+
if prop.enum or (prop.ref and self._is_enum_ref(prop.ref)):
|
317
|
+
return True
|
318
|
+
|
319
|
+
if schema.items:
|
320
|
+
if schema.items.enum or (schema.items.ref and self._is_enum_ref(schema.items.ref)):
|
321
|
+
return True
|
322
|
+
|
323
|
+
return False
|
324
|
+
|
325
|
+
def _is_enum_ref(self, ref: str) -> bool:
|
326
|
+
"""Check if reference points to an enum."""
|
327
|
+
if ref in self.context.schemas:
|
328
|
+
return self.context.schemas[ref].enum is not None
|
329
|
+
return False
|
@@ -0,0 +1,131 @@
|
|
1
|
+
/**
|
2
|
+
* Global API Instance - Singleton configuration
|
3
|
+
*
|
4
|
+
* This module provides a global API instance that can be configured once
|
5
|
+
* and used throughout your application.
|
6
|
+
*
|
7
|
+
* Usage:
|
8
|
+
* ```typescript
|
9
|
+
* // Configure once (e.g., in your app entry point)
|
10
|
+
* import { configureAPI } from './api-instance'
|
11
|
+
*
|
12
|
+
* configureAPI({
|
13
|
+
* baseUrl: 'https://api.example.com',
|
14
|
+
* token: 'your-jwt-token'
|
15
|
+
* })
|
16
|
+
*
|
17
|
+
* // Then use fetchers and hooks anywhere without configuration
|
18
|
+
* import { getUsers } from './fetchers'
|
19
|
+
* const users = await getUsers({ page: 1 })
|
20
|
+
* ```
|
21
|
+
*
|
22
|
+
* For SSR or multiple instances:
|
23
|
+
* ```typescript
|
24
|
+
* import { API } from './index'
|
25
|
+
* import { getUsers } from './fetchers'
|
26
|
+
*
|
27
|
+
* const api = new API('https://api.example.com')
|
28
|
+
* const users = await getUsers({ page: 1 }, api)
|
29
|
+
* ```
|
30
|
+
*/
|
31
|
+
|
32
|
+
import { API, type APIOptions } from './index'
|
33
|
+
|
34
|
+
let globalAPI: API | null = null
|
35
|
+
|
36
|
+
/**
|
37
|
+
* Get the global API instance
|
38
|
+
* @throws Error if API is not configured
|
39
|
+
*/
|
40
|
+
export function getAPIInstance(): API {
|
41
|
+
if (!globalAPI) {
|
42
|
+
throw new Error(
|
43
|
+
'API not configured. Call configureAPI() with your base URL before using fetchers or hooks.\n\n' +
|
44
|
+
'Example:\n' +
|
45
|
+
' import { configureAPI } from "./api-instance"\n' +
|
46
|
+
' configureAPI({ baseUrl: "https://api.example.com" })'
|
47
|
+
)
|
48
|
+
}
|
49
|
+
return globalAPI
|
50
|
+
}
|
51
|
+
|
52
|
+
/**
|
53
|
+
* Check if API is configured
|
54
|
+
*/
|
55
|
+
export function isAPIConfigured(): boolean {
|
56
|
+
return globalAPI !== null
|
57
|
+
}
|
58
|
+
|
59
|
+
/**
|
60
|
+
* Configure the global API instance
|
61
|
+
*
|
62
|
+
* @param baseUrl - Base URL for the API
|
63
|
+
* @param options - Optional configuration (storage, retry, logger)
|
64
|
+
*
|
65
|
+
* @example
|
66
|
+
* ```typescript
|
67
|
+
* configureAPI({
|
68
|
+
* baseUrl: 'https://api.example.com',
|
69
|
+
* token: 'jwt-token',
|
70
|
+
* options: {
|
71
|
+
* retryConfig: { maxRetries: 3 },
|
72
|
+
* loggerConfig: { enabled: true }
|
73
|
+
* }
|
74
|
+
* })
|
75
|
+
* ```
|
76
|
+
*/
|
77
|
+
export function configureAPI(config: {
|
78
|
+
baseUrl: string
|
79
|
+
token?: string
|
80
|
+
refreshToken?: string
|
81
|
+
options?: APIOptions
|
82
|
+
}): API {
|
83
|
+
globalAPI = new API(config.baseUrl, config.options)
|
84
|
+
|
85
|
+
if (config.token) {
|
86
|
+
globalAPI.setToken(config.token, config.refreshToken)
|
87
|
+
}
|
88
|
+
|
89
|
+
return globalAPI
|
90
|
+
}
|
91
|
+
|
92
|
+
/**
|
93
|
+
* Reconfigure the global API instance with new settings
|
94
|
+
* Useful for updating tokens or base URL
|
95
|
+
*/
|
96
|
+
export function reconfigureAPI(updates: {
|
97
|
+
baseUrl?: string
|
98
|
+
token?: string
|
99
|
+
refreshToken?: string
|
100
|
+
}): API {
|
101
|
+
const instance = getAPIInstance()
|
102
|
+
|
103
|
+
if (updates.baseUrl) {
|
104
|
+
instance.setBaseUrl(updates.baseUrl)
|
105
|
+
}
|
106
|
+
|
107
|
+
if (updates.token) {
|
108
|
+
instance.setToken(updates.token, updates.refreshToken)
|
109
|
+
}
|
110
|
+
|
111
|
+
return instance
|
112
|
+
}
|
113
|
+
|
114
|
+
/**
|
115
|
+
* Clear tokens from the global API instance
|
116
|
+
*/
|
117
|
+
export function clearAPITokens(): void {
|
118
|
+
const instance = getAPIInstance()
|
119
|
+
instance.clearTokens()
|
120
|
+
}
|
121
|
+
|
122
|
+
/**
|
123
|
+
* Reset the global API instance
|
124
|
+
* Useful for testing or logout scenarios
|
125
|
+
*/
|
126
|
+
export function resetAPI(): void {
|
127
|
+
if (globalAPI) {
|
128
|
+
globalAPI.clearTokens()
|
129
|
+
}
|
130
|
+
globalAPI = null
|
131
|
+
}
|
@@ -7,6 +7,7 @@ import { {{ tag.class_name }} } from "./{{ tag.slug }}";
|
|
7
7
|
import { HttpClientAdapter, FetchAdapter } from "./http";
|
8
8
|
import { APIError, NetworkError } from "./errors";
|
9
9
|
import { APILogger, type LoggerConfig } from "./logger";
|
10
|
+
import { withRetry, type RetryConfig } from "./retry";
|
10
11
|
|
11
12
|
|
12
13
|
{% endif %}
|
@@ -29,6 +30,7 @@ export class APIClient {
|
|
29
30
|
private baseUrl: string;
|
30
31
|
private httpClient: HttpClientAdapter;
|
31
32
|
private logger: APILogger | null = null;
|
33
|
+
private retryConfig: RetryConfig | null = null;
|
32
34
|
|
33
35
|
// Sub-clients
|
34
36
|
{% for tag in tags %}
|
@@ -40,6 +42,7 @@ export class APIClient {
|
|
40
42
|
options?: {
|
41
43
|
httpClient?: HttpClientAdapter;
|
42
44
|
loggerConfig?: Partial<LoggerConfig>;
|
45
|
+
retryConfig?: RetryConfig;
|
43
46
|
}
|
44
47
|
) {
|
45
48
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
@@ -50,6 +53,11 @@ export class APIClient {
|
|
50
53
|
this.logger = new APILogger(options.loggerConfig);
|
51
54
|
}
|
52
55
|
|
56
|
+
// Store retry configuration
|
57
|
+
if (options?.retryConfig !== undefined) {
|
58
|
+
this.retryConfig = options.retryConfig;
|
59
|
+
}
|
60
|
+
|
53
61
|
// Initialize sub-clients
|
54
62
|
{% for tag in tags %}
|
55
63
|
this.{{ tag.property }} = new {{ tag.class_name }}(this);
|
@@ -71,6 +79,7 @@ export class APIClient {
|
|
71
79
|
|
72
80
|
/**
|
73
81
|
* Make HTTP request with Django CSRF and session handling.
|
82
|
+
* Automatically retries on network errors and 5xx server errors.
|
74
83
|
*/
|
75
84
|
async request<T>(
|
76
85
|
method: string,
|
@@ -80,6 +89,41 @@ export class APIClient {
|
|
80
89
|
body?: any;
|
81
90
|
formData?: FormData;
|
82
91
|
}
|
92
|
+
): Promise<T> {
|
93
|
+
// Wrap request in retry logic if configured
|
94
|
+
if (this.retryConfig) {
|
95
|
+
return withRetry(() => this._makeRequest<T>(method, path, options), {
|
96
|
+
...this.retryConfig,
|
97
|
+
onFailedAttempt: (info) => {
|
98
|
+
// Log retry attempts
|
99
|
+
if (this.logger) {
|
100
|
+
this.logger.warn(
|
101
|
+
`Retry attempt ${info.attemptNumber}/${info.retriesLeft + info.attemptNumber} ` +
|
102
|
+
`for ${method} ${path}: ${info.error.message}`
|
103
|
+
);
|
104
|
+
}
|
105
|
+
// Call user's onFailedAttempt if provided
|
106
|
+
this.retryConfig?.onFailedAttempt?.(info);
|
107
|
+
},
|
108
|
+
});
|
109
|
+
}
|
110
|
+
|
111
|
+
// No retry configured, make request directly
|
112
|
+
return this._makeRequest<T>(method, path, options);
|
113
|
+
}
|
114
|
+
|
115
|
+
/**
|
116
|
+
* Internal request method (without retry wrapper).
|
117
|
+
* Used by request() method with optional retry logic.
|
118
|
+
*/
|
119
|
+
private async _makeRequest<T>(
|
120
|
+
method: string,
|
121
|
+
path: string,
|
122
|
+
options?: {
|
123
|
+
params?: Record<string, any>;
|
124
|
+
body?: any;
|
125
|
+
formData?: FormData;
|
126
|
+
}
|
83
127
|
): Promise<T> {
|
84
128
|
const url = new URL(path, this.baseUrl);
|
85
129
|
const startTime = Date.now();
|
@@ -216,6 +260,7 @@ export class APIClient {
|
|
216
260
|
import * as Enums from "./enums";
|
217
261
|
import { HttpClientAdapter, FetchAdapter } from "./http";
|
218
262
|
import { APIError, NetworkError } from "./errors";
|
263
|
+
import { withRetry, type RetryConfig } from "./retry";
|
219
264
|
|
220
265
|
|
221
266
|
/**
|
@@ -235,13 +280,22 @@ import { APIError, NetworkError } from "./errors";
|
|
235
280
|
export class APIClient {
|
236
281
|
private baseUrl: string;
|
237
282
|
private httpClient: HttpClientAdapter;
|
283
|
+
private retryConfig: RetryConfig | null = null;
|
238
284
|
|
239
285
|
constructor(
|
240
286
|
baseUrl: string,
|
241
|
-
options?: {
|
287
|
+
options?: {
|
288
|
+
httpClient?: HttpClientAdapter;
|
289
|
+
retryConfig?: RetryConfig;
|
290
|
+
}
|
242
291
|
) {
|
243
292
|
this.baseUrl = baseUrl.replace(/\/$/, '');
|
244
293
|
this.httpClient = options?.httpClient || new FetchAdapter();
|
294
|
+
|
295
|
+
// Store retry configuration
|
296
|
+
if (options?.retryConfig !== undefined) {
|
297
|
+
this.retryConfig = options.retryConfig;
|
298
|
+
}
|
245
299
|
}
|
246
300
|
|
247
301
|
/**
|
@@ -259,6 +313,7 @@ export class APIClient {
|
|
259
313
|
|
260
314
|
/**
|
261
315
|
* Make HTTP request with Django CSRF and session handling.
|
316
|
+
* Automatically retries on network errors and 5xx server errors.
|
262
317
|
*/
|
263
318
|
private async request<T>(
|
264
319
|
method: string,
|
@@ -268,6 +323,27 @@ export class APIClient {
|
|
268
323
|
body?: any;
|
269
324
|
formData?: FormData;
|
270
325
|
}
|
326
|
+
): Promise<T> {
|
327
|
+
// Wrap request in retry logic if configured
|
328
|
+
if (this.retryConfig) {
|
329
|
+
return withRetry(() => this._makeRequest<T>(method, path, options), this.retryConfig);
|
330
|
+
}
|
331
|
+
|
332
|
+
// No retry configured, make request directly
|
333
|
+
return this._makeRequest<T>(method, path, options);
|
334
|
+
}
|
335
|
+
|
336
|
+
/**
|
337
|
+
* Internal request method (without retry wrapper).
|
338
|
+
*/
|
339
|
+
private async _makeRequest<T>(
|
340
|
+
method: string,
|
341
|
+
path: string,
|
342
|
+
options?: {
|
343
|
+
params?: Record<string, any>;
|
344
|
+
body?: any;
|
345
|
+
formData?: FormData;
|
346
|
+
}
|
271
347
|
): Promise<T> {
|
272
348
|
const url = new URL(path, this.baseUrl);
|
273
349
|
|
@@ -4,6 +4,7 @@ import { {{ tag.class_name }} } from "./{{ tag.slug }}";
|
|
4
4
|
import { HttpClientAdapter, FetchAdapter } from "./http";
|
5
5
|
import { APIError, NetworkError } from "./errors";
|
6
6
|
import { APILogger, type LoggerConfig } from "./logger";
|
7
|
+
import { withRetry, type RetryConfig } from "./retry";
|
7
8
|
|
8
9
|
|
9
10
|
{{ client_code }}
|
@@ -2,14 +2,14 @@
|
|
2
2
|
* API endpoints for {{ tag }}.
|
3
3
|
*/
|
4
4
|
class {{ class_name }} {
|
5
|
-
private client:
|
5
|
+
private client: any;
|
6
6
|
|
7
|
-
constructor(client:
|
7
|
+
constructor(client: any) {
|
8
8
|
this.client = client;
|
9
9
|
}
|
10
10
|
|
11
11
|
{% for operation in operations %}
|
12
|
-
|
12
|
+
{{ operation }}
|
13
13
|
|
14
14
|
{% endfor %}
|
15
15
|
}
|
django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
/**
|
2
|
+
* Typed fetchers for {{ tag_display_name }}
|
3
|
+
*
|
4
|
+
* Universal functions that work in any environment:
|
5
|
+
* - Next.js (App Router / Pages Router / Server Components)
|
6
|
+
* - React Native
|
7
|
+
* - Node.js backend
|
8
|
+
*
|
9
|
+
* These fetchers use Zod schemas for runtime validation.
|
10
|
+
*
|
11
|
+
* Usage:
|
12
|
+
* ```typescript
|
13
|
+
* // Configure API once (in your app entry point)
|
14
|
+
* import { configureAPI } from '../../api-instance'
|
15
|
+
* configureAPI({ baseUrl: 'https://api.example.com' })
|
16
|
+
*
|
17
|
+
* // Then use fetchers anywhere
|
18
|
+
* const users = await getUsers({ page: 1 })
|
19
|
+
*
|
20
|
+
* // With SWR
|
21
|
+
* const { data } = useSWR(['users', params], () => getUsers(params))
|
22
|
+
*
|
23
|
+
* // With React Query
|
24
|
+
* const { data } = useQuery(['users', params], () => getUsers(params))
|
25
|
+
*
|
26
|
+
* // In Server Component or SSR (pass custom client)
|
27
|
+
* import { API } from '../../index'
|
28
|
+
* const api = new API('https://api.example.com')
|
29
|
+
* const users = await getUsers({ page: 1 }, api)
|
30
|
+
* ```
|
31
|
+
*/
|
32
|
+
{% if has_schemas %}
|
33
|
+
{% for schema_name in schema_names %}
|
34
|
+
import { {{ schema_name }}Schema, type {{ schema_name }} } from '../schemas/{{ schema_name }}.schema'
|
35
|
+
{% endfor %}
|
36
|
+
{% endif %}
|
37
|
+
{% if has_client %}
|
38
|
+
import { getAPIInstance } from '../../api-instance'
|
39
|
+
import type { API } from '../../index'
|
40
|
+
{% endif %}
|
41
|
+
|
42
|
+
{% for fetcher in fetchers %}
|
43
|
+
{{ fetcher }}
|
44
|
+
|
45
|
+
{% endfor %}
|