django-cfg 1.5.8__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 (159) hide show
  1. django_cfg/__init__.py +1 -1
  2. django_cfg/apps/api/commands/serializers.py +152 -0
  3. django_cfg/apps/api/commands/views.py +32 -0
  4. django_cfg/apps/business/accounts/management/commands/otp_test.py +5 -2
  5. django_cfg/apps/business/accounts/serializers/profile.py +42 -0
  6. django_cfg/apps/business/agents/management/commands/create_agent.py +5 -194
  7. django_cfg/apps/business/agents/management/commands/load_agent_templates.py +205 -0
  8. django_cfg/apps/business/agents/management/commands/orchestrator_status.py +4 -2
  9. django_cfg/apps/business/knowbase/management/commands/knowbase_stats.py +4 -2
  10. django_cfg/apps/business/knowbase/management/commands/setup_knowbase.py +4 -2
  11. django_cfg/apps/business/newsletter/management/commands/test_newsletter.py +5 -2
  12. django_cfg/apps/business/payments/management/commands/check_payment_status.py +4 -2
  13. django_cfg/apps/business/payments/management/commands/create_payment.py +4 -2
  14. django_cfg/apps/business/payments/management/commands/sync_currencies.py +4 -2
  15. django_cfg/apps/business/support/serializers.py +3 -2
  16. django_cfg/apps/integrations/centrifugo/apps.py +2 -1
  17. django_cfg/apps/integrations/centrifugo/codegen/generators/typescript_thin/templates/rpc-client.ts.j2 +151 -12
  18. django_cfg/apps/integrations/centrifugo/management/commands/generate_centrifugo_clients.py +6 -6
  19. django_cfg/apps/integrations/centrifugo/serializers/__init__.py +2 -1
  20. django_cfg/apps/integrations/centrifugo/serializers/publishes.py +22 -2
  21. django_cfg/apps/integrations/centrifugo/services/__init__.py +6 -0
  22. django_cfg/apps/integrations/centrifugo/services/client/__init__.py +6 -1
  23. django_cfg/apps/integrations/centrifugo/services/client/direct_client.py +282 -0
  24. django_cfg/apps/integrations/centrifugo/services/publisher.py +371 -0
  25. django_cfg/apps/integrations/centrifugo/services/token_generator.py +122 -0
  26. django_cfg/apps/integrations/centrifugo/urls.py +8 -0
  27. django_cfg/apps/integrations/centrifugo/views/__init__.py +2 -0
  28. django_cfg/apps/integrations/centrifugo/views/monitoring.py +25 -40
  29. django_cfg/apps/integrations/centrifugo/views/testing_api.py +0 -79
  30. django_cfg/apps/integrations/centrifugo/views/token_api.py +101 -0
  31. django_cfg/apps/integrations/centrifugo/views/wrapper.py +257 -0
  32. django_cfg/apps/integrations/grpc/admin/__init__.py +7 -1
  33. django_cfg/apps/integrations/grpc/admin/config.py +113 -9
  34. django_cfg/apps/integrations/grpc/admin/grpc_api_key.py +129 -0
  35. django_cfg/apps/integrations/grpc/admin/grpc_request_log.py +72 -63
  36. django_cfg/apps/integrations/grpc/admin/grpc_server_status.py +236 -0
  37. django_cfg/apps/integrations/grpc/auth/__init__.py +11 -3
  38. django_cfg/apps/integrations/grpc/auth/api_key_auth.py +320 -0
  39. django_cfg/apps/integrations/grpc/centrifugo/__init__.py +29 -0
  40. django_cfg/apps/integrations/grpc/centrifugo/bridge.py +277 -0
  41. django_cfg/apps/integrations/grpc/centrifugo/config.py +167 -0
  42. django_cfg/apps/integrations/grpc/centrifugo/demo.py +626 -0
  43. django_cfg/apps/integrations/grpc/centrifugo/test_publish.py +229 -0
  44. django_cfg/apps/integrations/grpc/centrifugo/transformers.py +89 -0
  45. django_cfg/apps/integrations/grpc/interceptors/__init__.py +3 -1
  46. django_cfg/apps/integrations/grpc/interceptors/centrifugo.py +541 -0
  47. django_cfg/apps/integrations/grpc/interceptors/logging.py +17 -20
  48. django_cfg/apps/integrations/grpc/interceptors/metrics.py +15 -14
  49. django_cfg/apps/integrations/grpc/interceptors/request_logger.py +79 -59
  50. django_cfg/apps/integrations/grpc/management/commands/compile_proto.py +105 -0
  51. django_cfg/apps/integrations/grpc/management/commands/generate_protos.py +185 -0
  52. django_cfg/apps/integrations/grpc/management/commands/rungrpc.py +474 -95
  53. django_cfg/apps/integrations/grpc/management/commands/test_grpc_integration.py +75 -0
  54. django_cfg/apps/integrations/grpc/management/proto/__init__.py +3 -0
  55. django_cfg/apps/integrations/grpc/management/proto/compiler.py +194 -0
  56. django_cfg/apps/integrations/grpc/managers/__init__.py +2 -0
  57. django_cfg/apps/integrations/grpc/managers/grpc_api_key.py +192 -0
  58. django_cfg/apps/integrations/grpc/managers/grpc_server_status.py +19 -11
  59. django_cfg/apps/integrations/grpc/migrations/0005_grpcapikey.py +143 -0
  60. django_cfg/apps/integrations/grpc/migrations/0006_grpcrequestlog_api_key_and_more.py +34 -0
  61. django_cfg/apps/integrations/grpc/models/__init__.py +2 -0
  62. django_cfg/apps/integrations/grpc/models/grpc_api_key.py +198 -0
  63. django_cfg/apps/integrations/grpc/models/grpc_request_log.py +11 -0
  64. django_cfg/apps/integrations/grpc/models/grpc_server_status.py +39 -4
  65. django_cfg/apps/integrations/grpc/serializers/__init__.py +22 -6
  66. django_cfg/apps/integrations/grpc/serializers/api_keys.py +63 -0
  67. django_cfg/apps/integrations/grpc/serializers/charts.py +118 -120
  68. django_cfg/apps/integrations/grpc/serializers/config.py +65 -51
  69. django_cfg/apps/integrations/grpc/serializers/health.py +7 -7
  70. django_cfg/apps/integrations/grpc/serializers/proto_files.py +74 -0
  71. django_cfg/apps/integrations/grpc/serializers/requests.py +13 -7
  72. django_cfg/apps/integrations/grpc/serializers/service_registry.py +181 -112
  73. django_cfg/apps/integrations/grpc/serializers/services.py +14 -32
  74. django_cfg/apps/integrations/grpc/serializers/stats.py +50 -12
  75. django_cfg/apps/integrations/grpc/serializers/testing.py +66 -58
  76. django_cfg/apps/integrations/grpc/services/__init__.py +2 -0
  77. django_cfg/apps/integrations/grpc/services/discovery.py +7 -1
  78. django_cfg/apps/integrations/grpc/services/monitoring_service.py +149 -43
  79. django_cfg/apps/integrations/grpc/services/proto_files_manager.py +268 -0
  80. django_cfg/apps/integrations/grpc/services/service_registry.py +48 -46
  81. django_cfg/apps/integrations/grpc/services/testing_service.py +10 -15
  82. django_cfg/apps/integrations/grpc/urls.py +8 -0
  83. django_cfg/apps/integrations/grpc/utils/SERVER_LOGGING.md +164 -0
  84. django_cfg/apps/integrations/grpc/utils/__init__.py +4 -13
  85. django_cfg/apps/integrations/grpc/utils/integration_test.py +334 -0
  86. django_cfg/apps/integrations/grpc/utils/proto_gen.py +48 -8
  87. django_cfg/apps/integrations/grpc/utils/streaming_logger.py +378 -0
  88. django_cfg/apps/integrations/grpc/views/__init__.py +4 -0
  89. django_cfg/apps/integrations/grpc/views/api_keys.py +255 -0
  90. django_cfg/apps/integrations/grpc/views/charts.py +21 -14
  91. django_cfg/apps/integrations/grpc/views/config.py +8 -6
  92. django_cfg/apps/integrations/grpc/views/monitoring.py +51 -79
  93. django_cfg/apps/integrations/grpc/views/proto_files.py +214 -0
  94. django_cfg/apps/integrations/grpc/views/services.py +30 -21
  95. django_cfg/apps/integrations/grpc/views/testing.py +45 -43
  96. django_cfg/apps/integrations/rq/views/jobs.py +19 -9
  97. django_cfg/apps/integrations/rq/views/schedule.py +7 -3
  98. django_cfg/apps/system/dashboard/serializers/commands.py +25 -1
  99. django_cfg/apps/system/dashboard/serializers/config.py +95 -9
  100. django_cfg/apps/system/dashboard/serializers/statistics.py +9 -4
  101. django_cfg/apps/system/dashboard/services/commands_service.py +12 -1
  102. django_cfg/apps/system/frontend/views.py +87 -6
  103. django_cfg/apps/system/maintenance/management/commands/maintenance.py +5 -2
  104. django_cfg/apps/system/maintenance/management/commands/process_scheduled_maintenance.py +4 -2
  105. django_cfg/apps/system/maintenance/management/commands/sync_cloudflare.py +5 -2
  106. django_cfg/config.py +33 -0
  107. django_cfg/core/builders/security_builder.py +1 -0
  108. django_cfg/core/generation/integration_generators/api.py +2 -0
  109. django_cfg/core/generation/integration_generators/grpc_generator.py +30 -32
  110. django_cfg/management/commands/check_endpoints.py +2 -2
  111. django_cfg/management/commands/check_settings.py +3 -10
  112. django_cfg/management/commands/clear_constance.py +3 -10
  113. django_cfg/management/commands/create_token.py +4 -11
  114. django_cfg/management/commands/list_urls.py +4 -10
  115. django_cfg/management/commands/migrate_all.py +18 -12
  116. django_cfg/management/commands/migrator.py +4 -11
  117. django_cfg/management/commands/script.py +4 -10
  118. django_cfg/management/commands/show_config.py +8 -16
  119. django_cfg/management/commands/show_urls.py +5 -11
  120. django_cfg/management/commands/superuser.py +4 -11
  121. django_cfg/management/commands/tree.py +5 -10
  122. django_cfg/management/utils/README.md +402 -0
  123. django_cfg/management/utils/__init__.py +29 -0
  124. django_cfg/management/utils/mixins.py +176 -0
  125. django_cfg/middleware/pagination.py +53 -54
  126. django_cfg/models/api/grpc/__init__.py +15 -21
  127. django_cfg/models/api/grpc/config.py +155 -73
  128. django_cfg/models/ngrok/config.py +7 -6
  129. django_cfg/modules/django_client/core/generator/python/files_generator.py +5 -13
  130. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +16 -4
  131. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +2 -3
  132. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +6 -5
  133. django_cfg/modules/django_client/core/generator/typescript/generator.py +26 -0
  134. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +7 -1
  135. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +5 -0
  136. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +11 -0
  137. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +1 -0
  138. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/function.ts.jinja +29 -1
  139. django_cfg/modules/django_client/core/generator/typescript/templates/hooks/hooks.ts.jinja +4 -0
  140. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +12 -8
  141. django_cfg/modules/django_client/core/ir/schema.py +15 -1
  142. django_cfg/modules/django_client/core/parser/base.py +126 -30
  143. django_cfg/modules/django_client/management/commands/generate_client.py +5 -2
  144. django_cfg/modules/django_client/management/commands/validate_openapi.py +5 -2
  145. django_cfg/modules/django_email/management/commands/test_email.py +4 -10
  146. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +16 -13
  147. django_cfg/modules/django_telegram/management/commands/test_telegram.py +4 -11
  148. django_cfg/modules/django_twilio/management/commands/test_twilio.py +4 -11
  149. django_cfg/modules/django_unfold/navigation.py +6 -18
  150. django_cfg/pyproject.toml +1 -1
  151. django_cfg/registry/modules.py +1 -4
  152. django_cfg/requirements.txt +52 -0
  153. django_cfg/static/frontend/admin.zip +0 -0
  154. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/METADATA +1 -1
  155. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/RECORD +158 -121
  156. django_cfg/apps/integrations/grpc/auth/jwt_auth.py +0 -295
  157. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/WHEEL +0 -0
  158. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/entry_points.txt +0 -0
  159. {django_cfg-1.5.8.dist-info → django_cfg-1.5.20.dist-info}/licenses/LICENSE +0 -0
@@ -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 %}
@@ -33,7 +33,6 @@
33
33
  */
34
34
 
35
35
  import { APIClient } from "./client";
36
- import { OPENAPI_SCHEMA } from "./schema";
37
36
  import {
38
37
  StorageAdapter,
39
38
  LocalStorageAdapter,
@@ -82,9 +81,6 @@ export * as Hooks from "./_utils/hooks";
82
81
  // Re-export core client
83
82
  export { APIClient };
84
83
 
85
- // Re-export OpenAPI schema
86
- export { OPENAPI_SCHEMA };
87
-
88
84
  // Re-export storage adapters for convenience
89
85
  export type { StorageAdapter };
90
86
  export { LocalStorageAdapter, CookieStorageAdapter, MemoryStorageAdapter };
@@ -267,11 +263,19 @@ export class API {
267
263
  }
268
264
 
269
265
  /**
270
- * Get OpenAPI schema
271
- * @returns Complete OpenAPI specification for this API
266
+ * Get OpenAPI schema path
267
+ * @returns Path to the OpenAPI schema JSON file
268
+ *
269
+ * Note: The OpenAPI schema is available in the schema.json file.
270
+ * You can load it dynamically using:
271
+ * ```typescript
272
+ * const schema = await fetch('./schema.json').then(r => r.json());
273
+ * // or using fs in Node.js:
274
+ * // const schema = JSON.parse(fs.readFileSync('./schema.json', 'utf-8'));
275
+ * ```
272
276
  */
273
- getSchema(): any {
274
- return OPENAPI_SCHEMA;
277
+ getSchemaPath(): string {
278
+ return './schema.json';
275
279
  }
276
280
  }
277
281
 
@@ -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",
@@ -62,8 +62,12 @@ class BaseParser(ABC):
62
62
  IRContext with all schemas and operations
63
63
 
64
64
  Raises:
65
- ValueError: If COMPONENT_SPLIT_REQUEST is not detected
65
+ ValueError: If COMPONENT_SPLIT_REQUEST is not detected or schema name conflicts found
66
66
  """
67
+ # CRITICAL: Validate schema names BEFORE parsing
68
+ # This prevents generation from starting with invalid schemas
69
+ self._validate_schema_names()
70
+
67
71
  # Parse metadata
68
72
  openapi_info = self._parse_openapi_info()
69
73
  django_metadata = self._parse_django_metadata()
@@ -237,54 +241,134 @@ class BaseParser(ABC):
237
241
 
238
242
  return False
239
243
 
240
- # ===== Schema Parsing =====
244
+ # ===== Schema Validation =====
245
+
246
+ def _validate_schema_names(self) -> None:
247
+ """
248
+ Validate schema names for conflicts BEFORE parsing.
249
+
250
+ This method checks for:
251
+ 1. Case-insensitive duplicates (e.g., "User" and "user")
252
+ 2. Exact duplicates (e.g., "GRPCServerInfo" from two different serializers)
253
+
254
+ Raises:
255
+ ValueError: If schema name conflicts are detected (hard exit with traceback)
256
+
257
+ Example conflicts:
258
+ - Case-insensitive: "Profile" and "profile" → filesystem conflict
259
+ - Exact duplicate: "HealthCheck" from multiple serializers → schema conflict
260
+ """
261
+ import traceback
262
+ import sys
241
263
 
242
- def _parse_all_schemas(self) -> dict[str, IRSchemaObject]:
243
- """Parse all schemas from components."""
244
264
  if not self.spec.components or not self.spec.components.schemas:
245
- return {}
265
+ return # No schemas to validate
246
266
 
247
- # Check for duplicate schema names with different casing
248
267
  schema_names = list(self.spec.components.schemas.keys())
249
268
  lowercase_map = {}
250
- exact_duplicate_map = {}
269
+ exact_duplicate_sources = {}
251
270
 
252
271
  for name in schema_names:
253
272
  lowercase = name.lower()
254
273
 
255
274
  # Check case-insensitive duplicates
256
275
  if lowercase in lowercase_map:
257
- raise ValueError(
258
- f"Duplicate schema names with different casing detected:\n"
259
- f" - {lowercase_map[lowercase]}\n"
260
- f" - {name}\n"
261
- f"This causes conflicts in case-insensitive file systems.\n"
262
- f"Please rename one of the serializers in your Django code."
276
+ existing_name = lowercase_map[lowercase]
277
+ error_msg = (
278
+ f"\n{'=' * 80}\n"
279
+ f" SCHEMA NAME CONFLICT DETECTED\n"
280
+ f"{'=' * 80}\n\n"
281
+ f"Conflict Type: Case-insensitive duplicate\n"
282
+ f"Schema Names:\n"
283
+ f" 1. '{existing_name}'\n"
284
+ f" 2. '{name}'\n\n"
285
+ f"Problem:\n"
286
+ f" These names differ only in casing and will cause conflicts on\n"
287
+ f" case-insensitive filesystems (macOS, Windows).\n\n"
288
+ f"Solution:\n"
289
+ f" Rename one of the Django serializers to make them distinct.\n"
290
+ f" Example: {name}Serializer → {name}DetailSerializer\n\n"
291
+ f"{'=' * 80}\n"
263
292
  )
264
- lowercase_map[lowercase] = name
265
293
 
266
- # Check exact duplicates
267
- if name in exact_duplicate_map:
268
- # Get schema titles to show source serializer names
269
- schema1 = self.spec.components.schemas.get(name)
270
- title1 = getattr(schema1, 'title', 'Unknown')
271
- title2 = exact_duplicate_map[name]
294
+ # Print full error with traceback
295
+ print(error_msg, file=sys.stderr)
296
+ print("\n🔍 Traceback (schema validation):", file=sys.stderr)
297
+ traceback.print_stack(file=sys.stderr)
272
298
 
299
+ # Hard exit - stop generation immediately
273
300
  raise ValueError(
274
- f"Duplicate schema name detected: '{name}'\n"
275
- f"Multiple serializers are generating the same schema name:\n"
276
- f" - {title1}\n"
277
- f" - {title2}\n"
278
- f"This causes schema conflicts in the generated API client.\n"
279
- f"Please rename one of the serializers to make them unique.\n"
280
- f"Example: HealthCheckSerializer → GRPCHealthCheckSerializer"
301
+ f"Case-insensitive schema name conflict: '{existing_name}' vs '{name}'. "
302
+ f"Cannot generate client with conflicting schema names."
281
303
  )
282
304
 
283
- # Store with title for later comparison
305
+ lowercase_map[lowercase] = name
306
+
307
+ # Track schema sources for exact duplicate detection
284
308
  schema = self.spec.components.schemas.get(name)
285
- title = getattr(schema, 'title', name)
286
- exact_duplicate_map[name] = title
309
+ if schema and not isinstance(schema, ReferenceObject):
310
+ # Get source information from schema
311
+ title = getattr(schema, 'title', name)
312
+ description = getattr(schema, 'description', '')
313
+
314
+ # Create signature for duplicate detection
315
+ if name in exact_duplicate_sources:
316
+ # Found exact duplicate!
317
+ existing = exact_duplicate_sources[name]
318
+ error_msg = (
319
+ f"\n{'=' * 80}\n"
320
+ f"❌ SCHEMA NAME CONFLICT DETECTED\n"
321
+ f"{'=' * 80}\n\n"
322
+ f"Conflict Type: Exact duplicate schema name\n"
323
+ f"Schema Name: '{name}'\n\n"
324
+ f"Sources:\n"
325
+ f" 1. {existing['title']}\n"
326
+ f" Description: {existing['description'][:100]}\n"
327
+ f" 2. {title}\n"
328
+ f" Description: {description[:100]}\n\n"
329
+ f"Problem:\n"
330
+ f" Multiple serializers are generating the same schema name '{name}'.\n"
331
+ f" This causes schema conflicts in the generated API client and\n"
332
+ f" OpenAPI documentation.\n\n"
333
+ f"Solution:\n"
334
+ f" Rename one of the serializers to make them unique.\n"
335
+ f" Examples:\n"
336
+ f" - HealthCheckSerializer → GRPCHealthCheckSerializer\n"
337
+ f" - ServerInfoSerializer → GRPCServerStatusSerializer\n\n"
338
+ f"{'=' * 80}\n"
339
+ )
340
+
341
+ # Print full error with traceback
342
+ print(error_msg, file=sys.stderr)
343
+ print("\n🔍 Traceback (schema validation):", file=sys.stderr)
344
+ traceback.print_stack(file=sys.stderr)
345
+
346
+ # Hard exit - stop generation immediately
347
+ raise ValueError(
348
+ f"Duplicate schema name detected: '{name}'. "
349
+ f"Multiple serializers are generating this schema. "
350
+ f"Cannot generate client with conflicting schemas."
351
+ )
352
+
353
+ # Store for duplicate detection
354
+ exact_duplicate_sources[name] = {
355
+ 'title': title,
356
+ 'description': description,
357
+ }
358
+
359
+ # ===== Schema Parsing =====
287
360
 
361
+ def _parse_all_schemas(self) -> dict[str, IRSchemaObject]:
362
+ """
363
+ Parse all schemas from components.
364
+
365
+ Note: Schema name validation is done in _validate_schema_names()
366
+ which is called BEFORE this method in parse().
367
+ """
368
+ if not self.spec.components or not self.spec.components.schemas:
369
+ return {}
370
+
371
+ # Parse schemas (validation already done in _validate_schema_names)
288
372
  schemas = {}
289
373
  for name, schema_or_ref in self.spec.components.schemas.items():
290
374
  # Skip references for now
@@ -369,6 +453,17 @@ class BaseParser(ABC):
369
453
  else:
370
454
  items = self._parse_schema(f"{name}.items", schema.items)
371
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
+
372
467
  # Create IR schema
373
468
  ir_schema = IRSchemaObject(
374
469
  name=name,
@@ -378,6 +473,7 @@ class BaseParser(ABC):
378
473
  nullable=self._detect_nullable(schema),
379
474
  properties=properties,
380
475
  required=schema.required or [],
476
+ additional_properties=additional_properties,
381
477
  items=items,
382
478
  enum=schema.enum,
383
479
  enum_var_names=schema.x_enum_varnames,
@@ -8,12 +8,15 @@ Usage:
8
8
  """
9
9
 
10
10
 
11
- from django.core.management.base import BaseCommand, CommandError
11
+ from django.core.management.base import CommandError
12
12
 
13
+ from django_cfg.management.utils import AdminCommand
13
14
 
14
- class Command(BaseCommand):
15
+
16
+ class Command(AdminCommand):
15
17
  """Generate OpenAPI clients for configured application groups."""
16
18
 
19
+ command_name = 'generate_client'
17
20
  help = "Generate Python, TypeScript, and Go API clients from OpenAPI schemas"
18
21
 
19
22
  def add_arguments(self, parser):
@@ -12,12 +12,15 @@ Usage:
12
12
  from pathlib import Path
13
13
  from typing import List
14
14
 
15
- from django.core.management.base import BaseCommand, CommandError
15
+ from django.core.management.base import CommandError
16
16
 
17
+ from django_cfg.management.utils import AdminCommand
17
18
 
18
- class Command(BaseCommand):
19
+
20
+ class Command(AdminCommand):
19
21
  """Validate and fix OpenAPI schema quality issues in DRF serializers."""
20
22
 
23
+ command_name = 'validate_openapi'
21
24
  help = "Validate and auto-fix OpenAPI schema quality issues"
22
25
 
23
26
  def add_arguments(self, parser):
@@ -5,22 +5,16 @@ Tests email sending functionality using django_cfg configuration.
5
5
  """
6
6
 
7
7
  from django.contrib.auth import get_user_model
8
- from django.core.management.base import BaseCommand
9
8
 
10
- from django_cfg.modules.django_logging import get_logger
9
+ from django_cfg.management.utils import SafeCommand
11
10
 
12
11
  User = get_user_model()
13
- logger = get_logger('test_email')
14
12
 
15
13
 
16
- class Command(BaseCommand):
14
+ class Command(SafeCommand):
17
15
  """Command to test email functionality."""
18
16
 
19
- # Web execution metadata
20
- web_executable = True
21
- requires_input = False
22
- is_destructive = False
23
-
17
+ command_name = 'test_email'
24
18
  help = "Test email sending functionality"
25
19
 
26
20
  def add_arguments(self, parser):
@@ -48,7 +42,7 @@ class Command(BaseCommand):
48
42
  subject = options["subject"]
49
43
  message = options["message"]
50
44
 
51
- logger.info(f"Starting email test for {email}")
45
+ self.logger.info(f"Starting email test for {email}")
52
46
  self.stdout.write(f"🚀 Testing email service for {email}")
53
47
 
54
48
  # Create test user if not exists
@@ -12,8 +12,6 @@ from django.core.management.commands.runserver import Command as RunServerComman
12
12
  from django_cfg.modules.django_logging import get_logger
13
13
  from django_cfg.modules.django_ngrok import get_ngrok_service
14
14
 
15
- logger = get_logger('runserver_ngrok')
16
-
17
15
 
18
16
  class Command(RunServerCommand):
19
17
  """Enhanced runserver command with ngrok tunnel support."""
@@ -25,6 +23,11 @@ class Command(RunServerCommand):
25
23
 
26
24
  help = f'{RunServerCommand.help.rstrip(".")} with ngrok tunnel.'
27
25
 
26
+ def __init__(self, *args, **kwargs):
27
+ """Initialize with logger."""
28
+ super().__init__(*args, **kwargs)
29
+ self.logger = get_logger('runserver_ngrok')
30
+
28
31
  def add_arguments(self, parser):
29
32
  super().add_arguments(parser)
30
33
  parser.add_argument(
@@ -76,14 +79,14 @@ class Command(RunServerCommand):
76
79
  ngrok_service = get_ngrok_service()
77
80
 
78
81
  self.stdout.write("🚇 Starting ngrok tunnel...")
79
- logger.info(f"Starting ngrok tunnel for port {server_port}")
82
+ self.logger.info(f"Starting ngrok tunnel for port {server_port}")
80
83
 
81
84
  tunnel_url = ngrok_service.start_tunnel(server_port)
82
85
 
83
86
  if tunnel_url:
84
87
  # Wait for tunnel to be fully established
85
88
  self.stdout.write("⏳ Waiting for tunnel to be established...")
86
- logger.info("Waiting for ngrok tunnel to be fully established")
89
+ self.logger.info("Waiting for ngrok tunnel to be fully established")
87
90
 
88
91
  max_retries = 10
89
92
  retry_count = 0
@@ -98,10 +101,10 @@ class Command(RunServerCommand):
98
101
  current_url = ngrok_service.get_tunnel_url()
99
102
  if current_url and current_url == tunnel_url:
100
103
  tunnel_ready = True
101
- logger.info(f"Ngrok tunnel established successfully: {tunnel_url}")
104
+ self.logger.info(f"Ngrok tunnel established successfully: {tunnel_url}")
102
105
  break
103
106
  except Exception as e:
104
- logger.warning(f"Tunnel check attempt {retry_count} failed: {e}")
107
+ self.logger.warning(f"Tunnel check attempt {retry_count} failed: {e}")
105
108
 
106
109
  self.stdout.write(f"⏳ Tunnel check {retry_count}/{max_retries}...")
107
110
 
@@ -116,16 +119,16 @@ class Command(RunServerCommand):
116
119
  self.stdout.write(
117
120
  self.style.SUCCESS(f"✅ Ngrok tunnel ready: {tunnel_url}")
118
121
  )
119
- logger.info(f"Ngrok tunnel fully ready: {tunnel_url}")
122
+ self.logger.info(f"Ngrok tunnel fully ready: {tunnel_url}")
120
123
  else:
121
124
  self.stdout.write(
122
125
  self.style.WARNING("⚠️ Ngrok tunnel started but may not be fully ready")
123
126
  )
124
- logger.warning("Ngrok tunnel started but readiness check failed")
127
+ self.logger.warning("Ngrok tunnel started but readiness check failed")
125
128
  else:
126
129
  error_msg = "Failed to start ngrok tunnel"
127
130
  self.stdout.write(self.style.ERROR(f"❌ {error_msg}"))
128
- logger.error(error_msg)
131
+ self.logger.error(error_msg)
129
132
 
130
133
  def _set_ngrok_env_vars(self, tunnel_url: str):
131
134
  """Set environment variables with ngrok URL for easy access."""
@@ -145,10 +148,10 @@ class Command(RunServerCommand):
145
148
  os.environ['NGROK_API_URL'] = tunnel_url
146
149
 
147
150
  # Environment variables set - no need for verbose output
148
- logger.info(f"Set ngrok environment variables: {tunnel_url}")
151
+ self.logger.info(f"Set ngrok environment variables: {tunnel_url}")
149
152
 
150
153
  except Exception as e:
151
- logger.warning(f"Could not set ngrok environment variables: {e}")
154
+ self.logger.warning(f"Could not set ngrok environment variables: {e}")
152
155
 
153
156
  def _update_allowed_hosts(self, tunnel_url: str):
154
157
  """Update ALLOWED_HOSTS with ngrok domain."""
@@ -164,7 +167,7 @@ class Command(RunServerCommand):
164
167
  if hasattr(settings, 'ALLOWED_HOSTS'):
165
168
  if ngrok_host not in settings.ALLOWED_HOSTS:
166
169
  settings.ALLOWED_HOSTS.append(ngrok_host)
167
- logger.info(f"Added {ngrok_host} to ALLOWED_HOSTS")
170
+ self.logger.info(f"Added {ngrok_host} to ALLOWED_HOSTS")
168
171
 
169
172
  except Exception as e:
170
- logger.warning(f"Could not update ALLOWED_HOSTS: {e}")
173
+ self.logger.warning(f"Could not update ALLOWED_HOSTS: {e}")
@@ -4,20 +4,13 @@ Test Telegram Command
4
4
  Tests Telegram notification functionality using django_cfg configuration.
5
5
  """
6
6
 
7
- from django.core.management.base import BaseCommand
7
+ from django_cfg.management.utils import SafeCommand
8
8
 
9
- from django_cfg.modules.django_logging import get_logger
10
9
 
11
- logger = get_logger('test_telegram')
12
-
13
- class Command(BaseCommand):
10
+ class Command(SafeCommand):
14
11
  """Command to test Telegram functionality."""
15
12
 
16
- # Web execution metadata
17
- web_executable = True
18
- requires_input = False
19
- is_destructive = False
20
-
13
+ command_name = 'test_telegram'
21
14
  help = "Test Telegram notification functionality"
22
15
 
23
16
  def add_arguments(self, parser):
@@ -29,7 +22,7 @@ class Command(BaseCommand):
29
22
  )
30
23
 
31
24
  def handle(self, *args, **options):
32
- logger.info("Starting test_telegram command")
25
+ self.logger.info("Starting test_telegram command")
33
26
  message = options["message"]
34
27
 
35
28
  self.stdout.write("🚀 Testing Telegram notification service")