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.

Files changed (53) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  3. django_cfg/apps/business/support/serializers.py +3 -2
  4. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  5. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  6. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +2 -2
  7. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  8. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  9. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  10. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  11. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  12. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  13. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  14. django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
  15. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  16. django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
  17. django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
  18. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
  19. django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
  20. django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
  21. django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
  22. django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
  23. django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
  24. django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
  25. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  26. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +55 -0
  27. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +311 -7
  28. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  29. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  30. django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
  31. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  32. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +206 -5
  33. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  34. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  35. django_cfg/apps/system/frontend/views.py +87 -6
  36. django_cfg/core/builders/security_builder.py +1 -0
  37. django_cfg/core/generation/integration_generators/api.py +2 -0
  38. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  39. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  40. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  41. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  42. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  43. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  44. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  45. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  46. django_cfg/modules/django_client/core/parser/base.py +12 -0
  47. django_cfg/pyproject.toml +1 -1
  48. django_cfg/static/frontend/admin.zip +0 -0
  49. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
  50. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/RECORD +53 -37
  51. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
  52. {django_cfg-1.5.14.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
  53. {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
- - Tokens injected into HTML responses only
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
- # Serve the static file
195
- response = serve(request, path, document_root=str(base_dir))
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.
@@ -323,6 +323,7 @@ class SecurityBuilder:
323
323
  """
324
324
  popular_ports = [
325
325
  3000, # React/Next.js default
326
+ 3777, # Next.js Admin default
326
327
  5173, # Vite default
327
328
  5174, # Vite preview
328
329
  8080, # Vue/Spring Boot
@@ -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({})"
@@ -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'
@@ -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
- return {{ response_schema }}.parse(response)
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
- if self.is_binary:
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.14"
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.14
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