django-cfg 1.5.14__py3-none-any.whl → 1.5.20__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.
Potentially problematic release.
This version of django-cfg might be problematic. Click here for more details.
- django_cfg/__init__.py +1 -1
- django_cfg/apps/business/accounts/serializers/profile.py +42 -0
- django_cfg/apps/business/support/serializers.py +3 -2
- django_cfg/apps/integrations/centrifugo/apps.py +2 -1
- django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
- django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
- django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
- django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
- django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
- django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
- django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
- django_cfg/apps/integrations/centrifugo/urls.py +8 -0
- django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
- django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
- django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
- django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
- django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
- django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
- django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
- django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
- django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
- django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
- django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
- django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
- django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
- django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +55 -0
- django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +311 -7
- django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
- django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
- django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
- django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
- django_cfg/apps/integrations/grpc/utils/streaming_logger.py +206 -5
- django_cfg/apps/system/dashboard/serializers/config.py +95 -9
- django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
- django_cfg/apps/system/frontend/views.py +87 -6
- django_cfg/core/builders/security_builder.py +1 -0
- django_cfg/core/generation/integration_generators/api.py +2 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
- django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
- django_cfg/modules/django_client/core/ir/schema.py +15 -1
- django_cfg/modules/django_client/core/parser/base.py +12 -0
- django_cfg/pyproject.toml +1 -1
- django_cfg/static/frontend/admin.zip +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/RECORD +53 -37
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
- {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
|
@@ -130,17 +130,23 @@ class ZipExtractionMixin:
|
|
|
130
130
|
@method_decorator(xframe_options_exempt, name='dispatch')
|
|
131
131
|
class NextJSStaticView(ZipExtractionMixin, View):
|
|
132
132
|
"""
|
|
133
|
-
Serve Next.js static build files with automatic JWT token injection.
|
|
133
|
+
Serve Next.js static build files with automatic JWT token injection and precompression support.
|
|
134
134
|
|
|
135
135
|
Features:
|
|
136
136
|
- Serves Next.js static export files like a static file server
|
|
137
137
|
- Smart ZIP extraction: compares ZIP metadata (size + mtime) with marker file
|
|
138
|
-
- Automatically injects JWT tokens for authenticated users
|
|
139
|
-
-
|
|
138
|
+
- Automatically injects JWT tokens for authenticated users (HTML only)
|
|
139
|
+
- **Precompression support**: Automatically serves .br or .gz files if available
|
|
140
140
|
- Handles Next.js client-side routing (.html fallback)
|
|
141
141
|
- Automatically serves index.html for directory paths
|
|
142
142
|
- X-Frame-Options exempt to allow embedding in iframes
|
|
143
143
|
|
|
144
|
+
Compression Strategy:
|
|
145
|
+
- Brotli (.br) preferred over Gzip (.gz) - ~5-15% better compression
|
|
146
|
+
- Automatically detects browser support via Accept-Encoding header
|
|
147
|
+
- Skips compression for HTML files (JWT injection requires uncompressed content)
|
|
148
|
+
- Only serves precompressed files, no runtime compression
|
|
149
|
+
|
|
144
150
|
ZIP Extraction Logic:
|
|
145
151
|
- If directory doesn't exist: extract from ZIP
|
|
146
152
|
- If marker file missing: extract from ZIP
|
|
@@ -154,12 +160,18 @@ class NextJSStaticView(ZipExtractionMixin, View):
|
|
|
154
160
|
- /cfg/admin/private/ → /cfg/admin/private.html (fallback)
|
|
155
161
|
- /cfg/admin/tasks → /cfg/admin/tasks.html
|
|
156
162
|
- /cfg/admin/tasks → /cfg/admin/tasks/index.html (fallback)
|
|
163
|
+
|
|
164
|
+
Compression examples:
|
|
165
|
+
- _app.js (br supported) → _app.js.br + Content-Encoding: br
|
|
166
|
+
- _app.js (gzip supported) → _app.js.gz + Content-Encoding: gzip
|
|
167
|
+
- _app.js (no support) → _app.js (uncompressed)
|
|
168
|
+
- index.html → index.html (never compressed, needs JWT injection)
|
|
157
169
|
"""
|
|
158
170
|
|
|
159
171
|
app_name = 'admin'
|
|
160
172
|
|
|
161
173
|
def get(self, request, path=''):
|
|
162
|
-
"""Serve static files from Next.js build with JWT injection."""
|
|
174
|
+
"""Serve static files from Next.js build with JWT injection and compression support."""
|
|
163
175
|
import django_cfg
|
|
164
176
|
|
|
165
177
|
base_dir = Path(django_cfg.__file__).parent / 'static' / 'frontend' / self.app_name
|
|
@@ -191,8 +203,18 @@ class NextJSStaticView(ZipExtractionMixin, View):
|
|
|
191
203
|
request.META.pop('HTTP_IF_MODIFIED_SINCE', None)
|
|
192
204
|
request.META.pop('HTTP_IF_NONE_MATCH', None)
|
|
193
205
|
|
|
194
|
-
#
|
|
195
|
-
|
|
206
|
+
# Try to serve precompressed file if browser supports it
|
|
207
|
+
compressed_path, encoding = self._find_precompressed_file(base_dir, path, request)
|
|
208
|
+
if compressed_path:
|
|
209
|
+
logger.debug(f"[Compression] Serving {encoding} for {path}")
|
|
210
|
+
response = serve(request, compressed_path, document_root=str(base_dir))
|
|
211
|
+
response['Content-Encoding'] = encoding
|
|
212
|
+
# Remove Content-Length as it's incorrect for compressed content
|
|
213
|
+
if 'Content-Length' in response:
|
|
214
|
+
del response['Content-Length']
|
|
215
|
+
else:
|
|
216
|
+
# Serve the static file normally
|
|
217
|
+
response = serve(request, path, document_root=str(base_dir))
|
|
196
218
|
|
|
197
219
|
# Convert FileResponse to HttpResponse for HTML files to enable JWT injection
|
|
198
220
|
if isinstance(response, FileResponse):
|
|
@@ -222,6 +244,65 @@ class NextJSStaticView(ZipExtractionMixin, View):
|
|
|
222
244
|
|
|
223
245
|
return response
|
|
224
246
|
|
|
247
|
+
def _find_precompressed_file(self, base_dir, path, request):
|
|
248
|
+
"""
|
|
249
|
+
Find and return precompressed file (.br or .gz) if available and supported by browser.
|
|
250
|
+
|
|
251
|
+
Brotli (.br) is preferred over Gzip (.gz) as it provides better compression.
|
|
252
|
+
|
|
253
|
+
Args:
|
|
254
|
+
base_dir: Base directory for static files
|
|
255
|
+
path: Requested file path
|
|
256
|
+
request: Django request object
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
tuple: (compressed_path, encoding) if precompressed file found and supported,
|
|
260
|
+
(None, None) otherwise
|
|
261
|
+
|
|
262
|
+
Examples:
|
|
263
|
+
_app.js → _app.js.br (if Accept-Encoding: br)
|
|
264
|
+
_app.js → _app.js.gz (if Accept-Encoding: gzip, no .br)
|
|
265
|
+
_app.js → (None, None) (if no precompressed files or not supported)
|
|
266
|
+
"""
|
|
267
|
+
# Get Accept-Encoding header
|
|
268
|
+
accept_encoding = request.META.get('HTTP_ACCEPT_ENCODING', '').lower()
|
|
269
|
+
|
|
270
|
+
# Check if browser supports brotli (preferred) or gzip
|
|
271
|
+
supports_br = 'br' in accept_encoding
|
|
272
|
+
supports_gzip = 'gzip' in accept_encoding
|
|
273
|
+
|
|
274
|
+
if not (supports_br or supports_gzip):
|
|
275
|
+
return None, None
|
|
276
|
+
|
|
277
|
+
# Don't compress HTML files - we need to inject JWT tokens
|
|
278
|
+
# JWT injection requires modifying content, which is incompatible with compression
|
|
279
|
+
if path.endswith('.html'):
|
|
280
|
+
return None, None
|
|
281
|
+
|
|
282
|
+
# Build full file path
|
|
283
|
+
file_path = base_dir / path
|
|
284
|
+
|
|
285
|
+
# Check if original file exists (safety check)
|
|
286
|
+
if not file_path.exists() or not file_path.is_file():
|
|
287
|
+
return None, None
|
|
288
|
+
|
|
289
|
+
# Try Brotli first (better compression, ~5-15% smaller than gzip)
|
|
290
|
+
if supports_br:
|
|
291
|
+
br_path = f"{path}.br"
|
|
292
|
+
br_file = base_dir / br_path
|
|
293
|
+
if br_file.exists() and br_file.is_file():
|
|
294
|
+
return br_path, 'br'
|
|
295
|
+
|
|
296
|
+
# Fallback to Gzip
|
|
297
|
+
if supports_gzip:
|
|
298
|
+
gz_path = f"{path}.gz"
|
|
299
|
+
gz_file = base_dir / gz_path
|
|
300
|
+
if gz_file.exists() and gz_file.is_file():
|
|
301
|
+
return gz_path, 'gzip'
|
|
302
|
+
|
|
303
|
+
# No precompressed file found or not supported
|
|
304
|
+
return None, None
|
|
305
|
+
|
|
225
306
|
def _resolve_spa_path(self, base_dir, path):
|
|
226
307
|
"""
|
|
227
308
|
Resolve SPA path with multiple fallback strategies.
|
|
@@ -125,6 +125,8 @@ class APIFrameworksGenerator:
|
|
|
125
125
|
],
|
|
126
126
|
# Add authentication classes from smart defaults
|
|
127
127
|
"DEFAULT_AUTHENTICATION_CLASSES": drf_defaults["DEFAULT_AUTHENTICATION_CLASSES"],
|
|
128
|
+
# Force ISO 8601 datetime format with Z suffix for all datetime fields
|
|
129
|
+
"DATETIME_FORMAT": "%Y-%m-%dT%H:%M:%SZ",
|
|
128
130
|
}
|
|
129
131
|
|
|
130
132
|
# Note: We don't set DEFAULT_PERMISSION_CLASSES here to allow public endpoints
|
|
@@ -298,6 +298,14 @@ class TypeScriptGenerator(BaseGenerator):
|
|
|
298
298
|
queue.append(self.context.schemas[prop.items.ref])
|
|
299
299
|
seen.add(prop.items.ref)
|
|
300
300
|
|
|
301
|
+
# $ref inside additionalProperties (CRITICAL for Record<string, T> patterns!)
|
|
302
|
+
if prop.additional_properties and prop.additional_properties.ref:
|
|
303
|
+
if prop.additional_properties.ref not in seen:
|
|
304
|
+
if prop.additional_properties.ref in self.context.schemas:
|
|
305
|
+
resolved[prop.additional_properties.ref] = self.context.schemas[prop.additional_properties.ref]
|
|
306
|
+
queue.append(self.context.schemas[prop.additional_properties.ref])
|
|
307
|
+
seen.add(prop.additional_properties.ref)
|
|
308
|
+
|
|
301
309
|
# Check array items for $ref at schema level
|
|
302
310
|
if schema.items and schema.items.ref:
|
|
303
311
|
if schema.items.ref not in seen:
|
|
@@ -306,6 +314,14 @@ class TypeScriptGenerator(BaseGenerator):
|
|
|
306
314
|
queue.append(self.context.schemas[schema.items.ref])
|
|
307
315
|
seen.add(schema.items.ref)
|
|
308
316
|
|
|
317
|
+
# Check additionalProperties for $ref at schema level
|
|
318
|
+
if schema.additional_properties and schema.additional_properties.ref:
|
|
319
|
+
if schema.additional_properties.ref not in seen:
|
|
320
|
+
if schema.additional_properties.ref in self.context.schemas:
|
|
321
|
+
resolved[schema.additional_properties.ref] = self.context.schemas[schema.additional_properties.ref]
|
|
322
|
+
queue.append(self.context.schemas[schema.additional_properties.ref])
|
|
323
|
+
seen.add(schema.additional_properties.ref)
|
|
324
|
+
|
|
309
325
|
return resolved
|
|
310
326
|
|
|
311
327
|
# ===== Zod Schemas Generation =====
|
|
@@ -369,11 +385,21 @@ class TypeScriptGenerator(BaseGenerator):
|
|
|
369
385
|
if not self.context.schemas[prop.items.ref].enum:
|
|
370
386
|
refs.add(prop.items.ref)
|
|
371
387
|
|
|
388
|
+
if prop.additional_properties and prop.additional_properties.ref:
|
|
389
|
+
if prop.additional_properties.ref in self.context.schemas:
|
|
390
|
+
if not self.context.schemas[prop.additional_properties.ref].enum:
|
|
391
|
+
refs.add(prop.additional_properties.ref)
|
|
392
|
+
|
|
372
393
|
if schema.items and schema.items.ref:
|
|
373
394
|
if schema.items.ref in self.context.schemas:
|
|
374
395
|
if not self.context.schemas[schema.items.ref].enum:
|
|
375
396
|
refs.add(schema.items.ref)
|
|
376
397
|
|
|
398
|
+
if schema.additional_properties and schema.additional_properties.ref:
|
|
399
|
+
if schema.additional_properties.ref in self.context.schemas:
|
|
400
|
+
if not self.context.schemas[schema.additional_properties.ref].enum:
|
|
401
|
+
refs.add(schema.additional_properties.ref)
|
|
402
|
+
|
|
377
403
|
return refs
|
|
378
404
|
|
|
379
405
|
# ===== Fetchers Generation =====
|
|
@@ -363,6 +363,8 @@ class HooksGenerator:
|
|
|
363
363
|
# Separate queries and mutations & collect schema names
|
|
364
364
|
hooks = []
|
|
365
365
|
schema_names = set()
|
|
366
|
+
has_queries = False
|
|
367
|
+
has_mutations = False
|
|
366
368
|
|
|
367
369
|
for operation in operations:
|
|
368
370
|
# Collect schemas used in this operation (only if they exist as components)
|
|
@@ -378,11 +380,13 @@ class HooksGenerator:
|
|
|
378
380
|
if response and response.schema_name:
|
|
379
381
|
schema_names.add(response.schema_name)
|
|
380
382
|
|
|
381
|
-
# Generate hook
|
|
383
|
+
# Generate hook and track operation types
|
|
382
384
|
if operation.http_method == "GET":
|
|
383
385
|
hooks.append(self.generate_query_hook(operation))
|
|
386
|
+
has_queries = True
|
|
384
387
|
else:
|
|
385
388
|
hooks.append(self.generate_mutation_hook(operation))
|
|
389
|
+
has_mutations = True
|
|
386
390
|
|
|
387
391
|
# Get display name for documentation
|
|
388
392
|
tag_display_name = self.base.tag_to_display_name(tag)
|
|
@@ -398,6 +402,8 @@ class HooksGenerator:
|
|
|
398
402
|
tag_file=tag_file,
|
|
399
403
|
has_schemas=bool(schema_names),
|
|
400
404
|
schema_names=sorted(schema_names),
|
|
405
|
+
has_queries=has_queries,
|
|
406
|
+
has_mutations=has_mutations,
|
|
401
407
|
hooks=hooks
|
|
402
408
|
)
|
|
403
409
|
|
|
@@ -175,8 +175,13 @@ class ModelsGenerator:
|
|
|
175
175
|
# Handle nullable and optional separately
|
|
176
176
|
# - nullable: add | null to type
|
|
177
177
|
# - not required: add ? optional marker
|
|
178
|
+
# Special case: readOnly + nullable fields should be optional
|
|
179
|
+
# (they're always in response but can be null, so from client perspective they're optional)
|
|
178
180
|
if schema.nullable:
|
|
179
181
|
ts_type = f"{ts_type} | null"
|
|
182
|
+
# Make readOnly nullable fields optional
|
|
183
|
+
if schema.read_only:
|
|
184
|
+
is_required = False
|
|
180
185
|
|
|
181
186
|
optional_marker = "" if is_required else "?"
|
|
182
187
|
|
|
@@ -237,6 +237,17 @@ class SchemasGenerator:
|
|
|
237
237
|
if schema.ref:
|
|
238
238
|
# Explicit reference
|
|
239
239
|
return f"{schema.ref}Schema"
|
|
240
|
+
elif schema.additional_properties:
|
|
241
|
+
# Object with additionalProperties (e.g., Record<string, DatabaseConfig>)
|
|
242
|
+
if schema.additional_properties.ref:
|
|
243
|
+
value_type = f"{schema.additional_properties.ref}Schema"
|
|
244
|
+
else:
|
|
245
|
+
value_type = self._map_type_to_zod(schema.additional_properties)
|
|
246
|
+
# If DictField() produces additionalProperties with just string type,
|
|
247
|
+
# use z.any() to allow mixed types (common in Django configs/settings)
|
|
248
|
+
if value_type == "z.string()":
|
|
249
|
+
value_type = "z.any()"
|
|
250
|
+
return f"z.record(z.string(), {value_type})"
|
|
240
251
|
elif schema.properties:
|
|
241
252
|
# Inline object with properties - shouldn't reach here, but use z.object
|
|
242
253
|
return "z.object({})"
|
django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja
CHANGED
|
@@ -29,6 +29,7 @@
|
|
|
29
29
|
* const users = await getUsers({ page: 1 }, api)
|
|
30
30
|
* ```
|
|
31
31
|
*/
|
|
32
|
+
import { consola } from 'consola'
|
|
32
33
|
{% if has_schemas %}
|
|
33
34
|
{% for schema_name in schema_names %}
|
|
34
35
|
import { {{ schema_name }}Schema, type {{ schema_name }} } from '../schemas/{{ schema_name }}.schema'
|
django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja
CHANGED
|
@@ -17,7 +17,35 @@ export async function {{ func_name }}(
|
|
|
17
17
|
const response = await {{ api_call }}()
|
|
18
18
|
{% endif %}
|
|
19
19
|
{% if response_schema %}
|
|
20
|
-
|
|
20
|
+
try {
|
|
21
|
+
return {{ response_schema }}.parse(response)
|
|
22
|
+
} catch (error) {
|
|
23
|
+
// Zod validation error - log detailed information
|
|
24
|
+
consola.error('❌ Zod Validation Failed');
|
|
25
|
+
consola.box({
|
|
26
|
+
title: '{{ func_name }}',
|
|
27
|
+
message: `Path: {{ operation.path }}\nMethod: {{ operation.http_method }}`,
|
|
28
|
+
style: {
|
|
29
|
+
borderColor: 'red',
|
|
30
|
+
borderStyle: 'rounded'
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (error instanceof Error && 'issues' in error && Array.isArray((error as any).issues)) {
|
|
35
|
+
consola.error('Validation Issues:');
|
|
36
|
+
(error as any).issues.forEach((issue: any, index: number) => {
|
|
37
|
+
consola.error(` ${index + 1}. ${issue.path.join('.') || 'root'}`);
|
|
38
|
+
consola.error(` ├─ Message: ${issue.message}`);
|
|
39
|
+
if (issue.expected) consola.error(` ├─ Expected: ${issue.expected}`);
|
|
40
|
+
if (issue.received) consola.error(` └─ Received: ${issue.received}`);
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
consola.error('Response data:', response);
|
|
45
|
+
|
|
46
|
+
// Re-throw the error
|
|
47
|
+
throw error;
|
|
48
|
+
}
|
|
21
49
|
{% else %}
|
|
22
50
|
return response
|
|
23
51
|
{% endif %}
|
|
@@ -14,8 +14,12 @@
|
|
|
14
14
|
* await createUser({ name: 'John', email: 'john@example.com' })
|
|
15
15
|
* ```
|
|
16
16
|
*/
|
|
17
|
+
{% if has_queries %}
|
|
17
18
|
import useSWR from 'swr'
|
|
19
|
+
{% endif %}
|
|
20
|
+
{% if has_mutations %}
|
|
18
21
|
import { useSWRConfig } from 'swr'
|
|
22
|
+
{% endif %}
|
|
19
23
|
import * as Fetchers from '../fetchers/{{ tag_file }}'
|
|
20
24
|
import type { API } from '../../index'
|
|
21
25
|
{% if has_schemas %}
|
|
@@ -103,6 +103,10 @@ class IRSchemaObject(BaseModel):
|
|
|
103
103
|
default_factory=list,
|
|
104
104
|
description="Required property names",
|
|
105
105
|
)
|
|
106
|
+
additional_properties: IRSchemaObject | None = Field(
|
|
107
|
+
None,
|
|
108
|
+
description="Schema for additional properties (for dynamic keys in object, e.g., Record<string, T>)",
|
|
109
|
+
)
|
|
106
110
|
|
|
107
111
|
# ===== Array Items =====
|
|
108
112
|
items: IRSchemaObject | None = Field(
|
|
@@ -322,8 +326,11 @@ class IRSchemaObject(BaseModel):
|
|
|
322
326
|
>>> IRSchemaObject(name="file", type="string", format="binary").typescript_type
|
|
323
327
|
'File | Blob'
|
|
324
328
|
"""
|
|
329
|
+
# Handle $ref (e.g., CentrifugoConfig, User, etc.)
|
|
330
|
+
if self.ref:
|
|
331
|
+
base_type = self.ref
|
|
325
332
|
# Handle binary type (file uploads)
|
|
326
|
-
|
|
333
|
+
elif self.is_binary:
|
|
327
334
|
base_type = "File | Blob"
|
|
328
335
|
# Handle array type with proper item type resolution
|
|
329
336
|
elif self.type == "array":
|
|
@@ -336,6 +343,13 @@ class IRSchemaObject(BaseModel):
|
|
|
336
343
|
base_type = f"Array<{item_type}>"
|
|
337
344
|
else:
|
|
338
345
|
base_type = "Array<any>"
|
|
346
|
+
# Handle object with additionalProperties (e.g., Record<string, DatabaseConfig>)
|
|
347
|
+
elif self.type == "object" and self.additional_properties:
|
|
348
|
+
if self.additional_properties.ref:
|
|
349
|
+
value_type = self.additional_properties.ref
|
|
350
|
+
else:
|
|
351
|
+
value_type = self.additional_properties.typescript_type
|
|
352
|
+
base_type = f"Record<string, {value_type}>"
|
|
339
353
|
else:
|
|
340
354
|
type_map = {
|
|
341
355
|
"string": "string",
|
|
@@ -453,6 +453,17 @@ class BaseParser(ABC):
|
|
|
453
453
|
else:
|
|
454
454
|
items = self._parse_schema(f"{name}.items", schema.items)
|
|
455
455
|
|
|
456
|
+
# Parse additionalProperties (for Record<string, T> types)
|
|
457
|
+
additional_properties = None
|
|
458
|
+
if schema.additionalProperties and not isinstance(schema.additionalProperties, bool):
|
|
459
|
+
if isinstance(schema.additionalProperties, ReferenceObject):
|
|
460
|
+
# Resolve reference (e.g., Record<string, DatabaseConfig>)
|
|
461
|
+
additional_properties = self._resolve_ref(schema.additionalProperties)
|
|
462
|
+
else:
|
|
463
|
+
additional_properties = self._parse_schema(
|
|
464
|
+
f"{name}.additionalProperties", schema.additionalProperties
|
|
465
|
+
)
|
|
466
|
+
|
|
456
467
|
# Create IR schema
|
|
457
468
|
ir_schema = IRSchemaObject(
|
|
458
469
|
name=name,
|
|
@@ -462,6 +473,7 @@ class BaseParser(ABC):
|
|
|
462
473
|
nullable=self._detect_nullable(schema),
|
|
463
474
|
properties=properties,
|
|
464
475
|
required=schema.required or [],
|
|
476
|
+
additional_properties=additional_properties,
|
|
465
477
|
items=items,
|
|
466
478
|
enum=schema.enum,
|
|
467
479
|
enum_var_names=schema.x_enum_varnames,
|
django_cfg/pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "django-cfg"
|
|
7
|
-
version = "1.5.
|
|
7
|
+
version = "1.5.20"
|
|
8
8
|
description = "Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
keywords = [ "django", "configuration", "pydantic", "settings", "type-safety", "pydantic-settings", "django-environ", "startup-validation", "ide-autocomplete", "nextjs-admin", "react-admin", "websocket", "centrifugo", "real-time", "typescript-generation", "ai-agents", "enterprise-django", "django-settings", "type-safe-config", "modern-django",]
|
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: django-cfg
|
|
3
|
-
Version: 1.5.
|
|
3
|
+
Version: 1.5.20
|
|
4
4
|
Summary: Modern Django framework with type-safe Pydantic v2 configuration, Next.js admin integration, real-time WebSockets, and 8 enterprise apps. Replace settings.py with validated models, 90% less code. Production-ready with AI agents, auto-generated TypeScript clients, and zero-config features.
|
|
5
5
|
Project-URL: Homepage, https://djangocfg.com
|
|
6
6
|
Project-URL: Documentation, https://djangocfg.com
|