django-cfg 1.4.10__py3-none-any.whl → 1.4.13__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. django_cfg/apps/agents/management/commands/create_agent.py +1 -1
  2. django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
  3. django_cfg/apps/newsletter/serializers.py +40 -3
  4. django_cfg/apps/newsletter/views/campaigns.py +12 -3
  5. django_cfg/apps/newsletter/views/emails.py +14 -3
  6. django_cfg/apps/newsletter/views/subscriptions.py +12 -2
  7. django_cfg/apps/payments/views/api/currencies.py +49 -6
  8. django_cfg/apps/payments/views/api/webhooks.py +72 -7
  9. django_cfg/apps/payments/views/overview/serializers.py +34 -1
  10. django_cfg/apps/payments/views/overview/views.py +2 -1
  11. django_cfg/apps/payments/views/serializers/payments.py +6 -6
  12. django_cfg/apps/urls.py +106 -45
  13. django_cfg/core/base/config_model.py +2 -2
  14. django_cfg/core/constants.py +1 -1
  15. django_cfg/core/generation/integration_generators/__init__.py +1 -1
  16. django_cfg/core/generation/integration_generators/api.py +73 -49
  17. django_cfg/core/integration/display/startup.py +30 -22
  18. django_cfg/core/integration/url_integration.py +15 -16
  19. django_cfg/management/commands/check_endpoints.py +11 -160
  20. django_cfg/management/commands/check_settings.py +13 -348
  21. django_cfg/management/commands/clear_constance.py +13 -201
  22. django_cfg/management/commands/create_token.py +13 -321
  23. django_cfg/management/commands/generate_clients.py +23 -0
  24. django_cfg/management/commands/list_urls.py +13 -306
  25. django_cfg/management/commands/migrate_all.py +13 -126
  26. django_cfg/management/commands/migrator.py +13 -396
  27. django_cfg/management/commands/rundramatiq.py +15 -247
  28. django_cfg/management/commands/rundramatiq_simulator.py +12 -429
  29. django_cfg/management/commands/runserver_ngrok.py +15 -160
  30. django_cfg/management/commands/script.py +12 -488
  31. django_cfg/management/commands/show_config.py +12 -215
  32. django_cfg/management/commands/show_urls.py +12 -342
  33. django_cfg/management/commands/superuser.py +15 -295
  34. django_cfg/management/commands/task_clear.py +14 -217
  35. django_cfg/management/commands/task_status.py +13 -248
  36. django_cfg/management/commands/test_email.py +15 -86
  37. django_cfg/management/commands/test_telegram.py +14 -61
  38. django_cfg/management/commands/test_twilio.py +15 -105
  39. django_cfg/management/commands/tree.py +13 -383
  40. django_cfg/management/commands/validate_openapi.py +10 -0
  41. django_cfg/middleware/README.md +1 -1
  42. django_cfg/middleware/user_activity.py +3 -3
  43. django_cfg/models/__init__.py +2 -2
  44. django_cfg/models/api/drf/spectacular.py +6 -6
  45. django_cfg/models/django/__init__.py +2 -2
  46. django_cfg/models/django/openapi.py +162 -0
  47. django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
  48. django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
  49. django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
  50. django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
  51. django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
  52. django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
  53. django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
  54. django_cfg/modules/django_admin/management/commands/script.py +496 -0
  55. django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
  56. django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
  57. django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
  58. django_cfg/modules/django_admin/management/commands/tree.py +390 -0
  59. django_cfg/modules/django_client/__init__.py +20 -0
  60. django_cfg/modules/django_client/apps.py +35 -0
  61. django_cfg/modules/django_client/core/__init__.py +56 -0
  62. django_cfg/modules/django_client/core/archive/__init__.py +11 -0
  63. django_cfg/modules/django_client/core/archive/manager.py +134 -0
  64. django_cfg/modules/django_client/core/cli/__init__.py +12 -0
  65. django_cfg/modules/django_client/core/cli/main.py +235 -0
  66. django_cfg/modules/django_client/core/config/__init__.py +18 -0
  67. django_cfg/modules/django_client/core/config/config.py +208 -0
  68. django_cfg/modules/django_client/core/config/group.py +101 -0
  69. django_cfg/modules/django_client/core/config/service.py +209 -0
  70. django_cfg/modules/django_client/core/generator/__init__.py +115 -0
  71. django_cfg/modules/django_client/core/generator/base.py +838 -0
  72. django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
  73. django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
  74. django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
  75. django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
  76. django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
  77. django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
  78. django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
  79. django_cfg/modules/django_client/core/generator/python/templates/__init__.py.jinja +9 -0
  80. django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +153 -0
  81. django_cfg/modules/django_client/core/generator/python/templates/app_init.py.jinja +6 -0
  82. django_cfg/modules/django_client/core/generator/python/templates/client/app_client.py.jinja +18 -0
  83. django_cfg/modules/django_client/core/generator/python/templates/client/flat_client.py.jinja +38 -0
  84. django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +68 -0
  85. django_cfg/modules/django_client/core/generator/python/templates/client/main_client_file.py.jinja +14 -0
  86. django_cfg/modules/django_client/core/generator/python/templates/client/operation_method.py.jinja +9 -0
  87. django_cfg/modules/django_client/core/generator/python/templates/client/sub_client.py.jinja +18 -0
  88. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
  89. django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
  90. django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
  91. django_cfg/modules/django_client/core/generator/python/templates/client_file.py.jinja +13 -0
  92. django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +52 -0
  93. django_cfg/modules/django_client/core/generator/python/templates/models/app_models.py.jinja +17 -0
  94. django_cfg/modules/django_client/core/generator/python/templates/models/enum_class.py.jinja +17 -0
  95. django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +8 -0
  96. django_cfg/modules/django_client/core/generator/python/templates/models/models.py.jinja +17 -0
  97. django_cfg/modules/django_client/core/generator/python/templates/models/schema_class.py.jinja +21 -0
  98. django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
  99. django_cfg/modules/django_client/core/generator/python/templates/utils/logger.py.jinja +255 -0
  100. django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
  101. django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py.jinja +12 -0
  102. django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
  103. django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
  104. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
  105. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
  106. django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
  107. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
  108. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
  109. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
  110. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
  111. django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
  112. django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja +2 -0
  113. django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja +18 -0
  114. django_cfg/modules/django_client/core/generator/typescript/templates/client/client.ts.jinja +403 -0
  115. django_cfg/modules/django_client/core/generator/typescript/templates/client/flat_client.ts.jinja +109 -0
  116. django_cfg/modules/django_client/core/generator/typescript/templates/client/main_client_file.ts.jinja +10 -0
  117. django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja +61 -0
  118. django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja +15 -0
  119. django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja +9 -0
  120. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
  121. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
  122. django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja +5 -0
  123. django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +268 -0
  124. django_cfg/modules/django_client/core/generator/typescript/templates/models/app_models.ts.jinja +8 -0
  125. django_cfg/modules/django_client/core/generator/typescript/templates/models/enums.ts.jinja +4 -0
  126. django_cfg/modules/django_client/core/generator/typescript/templates/models/models.ts.jinja +8 -0
  127. django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
  128. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
  129. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
  130. django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
  131. django_cfg/modules/django_client/core/generator/typescript/templates/utils/errors.ts.jinja +116 -0
  132. django_cfg/modules/django_client/core/generator/typescript/templates/utils/http.ts.jinja +98 -0
  133. django_cfg/modules/django_client/core/generator/typescript/templates/utils/logger.ts.jinja +259 -0
  134. django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
  135. django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja +7 -0
  136. django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja +158 -0
  137. django_cfg/modules/django_client/core/groups/__init__.py +13 -0
  138. django_cfg/modules/django_client/core/groups/detector.py +178 -0
  139. django_cfg/modules/django_client/core/groups/manager.py +314 -0
  140. django_cfg/modules/django_client/core/ir/__init__.py +57 -0
  141. django_cfg/modules/django_client/core/ir/context.py +387 -0
  142. django_cfg/modules/django_client/core/ir/operation.py +518 -0
  143. django_cfg/modules/django_client/core/ir/schema.py +353 -0
  144. django_cfg/modules/django_client/core/parser/__init__.py +74 -0
  145. django_cfg/modules/django_client/core/parser/base.py +648 -0
  146. django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
  147. django_cfg/modules/django_client/core/parser/models/base.py +212 -0
  148. django_cfg/modules/django_client/core/parser/models/components.py +160 -0
  149. django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
  150. django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
  151. django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
  152. django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
  153. django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
  154. django_cfg/modules/django_client/core/validation/__init__.py +22 -0
  155. django_cfg/modules/django_client/core/validation/checker.py +134 -0
  156. django_cfg/modules/django_client/core/validation/fixer.py +216 -0
  157. django_cfg/modules/django_client/core/validation/reporter.py +480 -0
  158. django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
  159. django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
  160. django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
  161. django_cfg/modules/django_client/core/validation/safety.py +266 -0
  162. django_cfg/modules/django_client/management/__init__.py +3 -0
  163. django_cfg/modules/django_client/management/commands/__init__.py +3 -0
  164. django_cfg/modules/django_client/management/commands/generate_client.py +427 -0
  165. django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
  166. django_cfg/modules/django_client/pytest.ini +30 -0
  167. django_cfg/modules/django_client/spectacular/__init__.py +10 -0
  168. django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
  169. django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
  170. django_cfg/modules/django_client/urls.py +72 -0
  171. django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
  172. django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
  173. django_cfg/modules/django_dashboard/management/__init__.py +0 -0
  174. django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
  175. django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
  176. django_cfg/modules/django_dashboard/sections/documentation.py +391 -0
  177. django_cfg/modules/django_email/management/__init__.py +0 -0
  178. django_cfg/modules/django_email/management/commands/__init__.py +0 -0
  179. django_cfg/modules/django_email/management/commands/test_email.py +93 -0
  180. django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
  181. django_cfg/modules/django_logging/django_logger.py +6 -6
  182. django_cfg/modules/django_ngrok/management/__init__.py +0 -0
  183. django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
  184. django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
  185. django_cfg/modules/django_tasks/management/__init__.py +0 -0
  186. django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
  187. django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
  188. django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
  189. django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
  190. django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
  191. django_cfg/modules/django_telegram/management/__init__.py +0 -0
  192. django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
  193. django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
  194. django_cfg/modules/django_twilio/management/__init__.py +0 -0
  195. django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
  196. django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
  197. django_cfg/modules/django_unfold/callbacks/main.py +21 -10
  198. django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
  199. django_cfg/pyproject.toml +2 -6
  200. django_cfg/registry/third_party.py +5 -7
  201. django_cfg/routing/callbacks.py +1 -1
  202. django_cfg/static/admin/css/prose-unfold.css +666 -0
  203. django_cfg/templates/admin/index.html +8 -0
  204. django_cfg/templates/admin/index_new.html +13 -0
  205. django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
  206. django_cfg/templates/admin/sections/documentation_section.html +172 -0
  207. django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
  208. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/METADATA +2 -2
  209. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/RECORD +224 -74
  210. django_cfg/management/commands/generate.py +0 -107
  211. /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
  212. /django_cfg/{dashboard → modules/django_admin}/management/__init__.py +0 -0
  213. /django_cfg/{dashboard → modules/django_admin}/management/commands/__init__.py +0 -0
  214. /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
  215. /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
  216. /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
  217. /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
  218. /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
  219. /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
  220. /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
  221. /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
  222. /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
  223. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
  224. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
  225. {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/licenses/LICENSE +0 -0
@@ -0,0 +1,318 @@
1
+ """
2
+ Models Generator - Generates Pydantic models and enums.
3
+
4
+ Handles:
5
+ - Pydantic 2 model classes (Request/Response/Patch)
6
+ - Enum classes (StrEnum, IntEnum)
7
+ - Field validation and constraints
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from jinja2 import Environment
13
+
14
+ from ..base import GeneratedFile
15
+ from ...ir import IRSchemaObject
16
+
17
+
18
+ class ModelsGenerator:
19
+ """Generates Pydantic models and enum classes."""
20
+
21
+ def __init__(self, jinja_env: Environment, context, base_generator):
22
+ """
23
+ Initialize models generator.
24
+
25
+ Args:
26
+ jinja_env: Jinja2 environment for templates
27
+ context: Generation context with schemas
28
+ base_generator: Reference to base generator for utility methods
29
+ """
30
+ self.jinja_env = jinja_env
31
+ self.context = context
32
+ self.base = base_generator
33
+
34
+ def generate_models_file(self) -> GeneratedFile:
35
+ """Generate models.py with all Pydantic models."""
36
+ # Generate all schemas
37
+ schema_codes = []
38
+
39
+ # Response models first
40
+ for name, schema in self.base.get_response_schemas().items():
41
+ schema_codes.append(self.generate_schema(schema))
42
+
43
+ # Request models
44
+ for name, schema in self.base.get_request_schemas().items():
45
+ schema_codes.append(self.generate_schema(schema))
46
+
47
+ # Patch models
48
+ for name, schema in self.base.get_patch_schemas().items():
49
+ schema_codes.append(self.generate_schema(schema))
50
+
51
+ template = self.jinja_env.get_template('models/models.py.jinja')
52
+ content = template.render(
53
+ has_enums=bool(self.base.get_enum_schemas()),
54
+ schemas=schema_codes
55
+ )
56
+
57
+ return GeneratedFile(
58
+ path="models.py",
59
+ content=content,
60
+ description="Pydantic 2 models (Request/Response/Patch)",
61
+ )
62
+
63
+ def generate_enums_file(self) -> GeneratedFile:
64
+ """Generate enums.py with all Enum classes (flat structure)."""
65
+ # Generate all enums
66
+ enum_codes = []
67
+ for name, schema in self.base.get_enum_schemas().items():
68
+ enum_codes.append(self.generate_enum(schema))
69
+
70
+ template = self.jinja_env.get_template('models/enums.py.jinja')
71
+ content = template.render(enums=enum_codes)
72
+
73
+ return GeneratedFile(
74
+ path="enums.py",
75
+ content=content,
76
+ description="Enum classes from x-enum-varnames",
77
+ )
78
+
79
+ def generate_shared_enums_file(self, enums: dict[str, IRSchemaObject]) -> GeneratedFile:
80
+ """Generate shared enums.py for namespaced structure (Variant 2)."""
81
+ # Generate all enums
82
+ enum_codes = []
83
+ for name, schema in enums.items():
84
+ enum_codes.append(self.generate_enum(schema))
85
+
86
+ template = self.jinja_env.get_template('models/enums.py.jinja')
87
+ content = template.render(enums=enum_codes)
88
+
89
+ return GeneratedFile(
90
+ path="enums.py",
91
+ content=content,
92
+ description="Shared enum classes from x-enum-varnames",
93
+ )
94
+
95
+ def generate_schema(self, schema: IRSchemaObject) -> str:
96
+ """Generate Pydantic model for schema."""
97
+ if schema.type != "object":
98
+ # For primitive types, skip (they'll be inlined)
99
+ return ""
100
+
101
+ # Class docstring
102
+ docstring_lines = []
103
+ if schema.description:
104
+ docstring_lines.extend(self.base.wrap_comment(schema.description, 76))
105
+
106
+ # Add metadata about model type
107
+ if schema.is_request_model:
108
+ docstring_lines.append("")
109
+ docstring_lines.append("Request model (no read-only fields).")
110
+ elif schema.is_patch_model:
111
+ docstring_lines.append("")
112
+ docstring_lines.append("PATCH model (all fields optional).")
113
+ elif schema.is_response_model:
114
+ docstring_lines.append("")
115
+ docstring_lines.append("Response model (includes read-only fields).")
116
+
117
+ docstring = "\n".join(docstring_lines) if docstring_lines else None
118
+
119
+ # Fields
120
+ field_lines = []
121
+ for prop_name, prop_schema in schema.properties.items():
122
+ field_lines.append(self._generate_field(prop_name, prop_schema, schema.required))
123
+
124
+ template = self.jinja_env.get_template('models/schema_class.py.jinja')
125
+ return template.render(
126
+ name=schema.name,
127
+ docstring=docstring,
128
+ fields=field_lines
129
+ )
130
+
131
+ def _generate_field(
132
+ self,
133
+ name: str,
134
+ schema: IRSchemaObject,
135
+ required_fields: list[str],
136
+ ) -> str:
137
+ """
138
+ Generate Pydantic field definition.
139
+
140
+ Examples:
141
+ id: int
142
+ username: str
143
+ email: str | None = None
144
+ age: int = Field(..., ge=0, le=150)
145
+ status: StatusEnum
146
+ """
147
+ # Check if this field is an enum
148
+ if schema.enum and schema.name:
149
+ # Use enum type from shared enums (sanitized to PascalCase)
150
+ python_type = self.base.sanitize_enum_name(schema.name)
151
+ if schema.nullable:
152
+ python_type = f"{python_type} | None"
153
+ # Check if this field is a reference to an enum (via $ref)
154
+ elif schema.ref and schema.ref in self.context.schemas:
155
+ ref_schema = self.context.schemas[schema.ref]
156
+ if ref_schema.enum:
157
+ # This is a reference to an enum component (sanitized to PascalCase)
158
+ python_type = self.base.sanitize_enum_name(schema.ref)
159
+ if schema.nullable:
160
+ python_type = f"{python_type} | None"
161
+ else:
162
+ # Regular reference
163
+ python_type = schema.python_type
164
+ else:
165
+ # Get Python type
166
+ python_type = schema.python_type
167
+
168
+ # Check if required
169
+ is_required = name in required_fields
170
+
171
+ # Build Field() kwargs
172
+ field_kwargs = []
173
+
174
+ if schema.description:
175
+ field_kwargs.append(f"description={schema.description!r}")
176
+
177
+ # Validation constraints
178
+ if schema.min_length is not None:
179
+ field_kwargs.append(f"min_length={schema.min_length}")
180
+ if schema.max_length is not None:
181
+ field_kwargs.append(f"max_length={schema.max_length}")
182
+ if schema.pattern:
183
+ field_kwargs.append(f"pattern={schema.pattern!r}")
184
+ if schema.minimum is not None:
185
+ field_kwargs.append(f"ge={schema.minimum}")
186
+ if schema.maximum is not None:
187
+ field_kwargs.append(f"le={schema.maximum}")
188
+
189
+ # Example
190
+ if schema.example:
191
+ field_kwargs.append(f"examples=[{schema.example!r}]")
192
+
193
+ # Default value
194
+ if is_required:
195
+ if field_kwargs:
196
+ default = f"Field({', '.join(field_kwargs)})"
197
+ else:
198
+ default = "..."
199
+ else:
200
+ if field_kwargs:
201
+ default = f"Field(None, {', '.join(field_kwargs)})"
202
+ else:
203
+ default = "None"
204
+
205
+ return f"{name}: {python_type} = {default}"
206
+
207
+ def generate_enum(self, schema: IRSchemaObject) -> str:
208
+ """Generate Enum class from x-enum-varnames."""
209
+ # Determine enum base class
210
+ if schema.type == "integer":
211
+ base_class = "IntEnum"
212
+ else:
213
+ base_class = "StrEnum"
214
+
215
+ # Sanitize enum class name (convert to PascalCase)
216
+ # "OrderDetail.status" → "OrderDetailStatus"
217
+ # "Currency.currency_type" → "CurrencyCurrencyType"
218
+ enum_name = self.base.sanitize_enum_name(schema.name)
219
+
220
+ # Class docstring
221
+ docstring = None
222
+ if schema.description:
223
+ # Format enum description to split bullet points
224
+ docstring = self.base.format_enum_description(schema.description)
225
+
226
+ # Enum members
227
+ member_lines = []
228
+ for var_name, value in zip(schema.enum_var_names, schema.enum):
229
+ # Skip empty values (from blank=True in Django)
230
+ if not var_name or (isinstance(value, str) and value == ''):
231
+ continue
232
+
233
+ if isinstance(value, str):
234
+ member_lines.append(f'{var_name} = "{value}"')
235
+ else:
236
+ member_lines.append(f"{var_name} = {value}")
237
+
238
+ template = self.jinja_env.get_template('models/enum_class.py.jinja')
239
+ return template.render(
240
+ name=enum_name,
241
+ base_class=base_class,
242
+ docstring=docstring,
243
+ members=member_lines
244
+ )
245
+
246
+ def generate_app_models_file(
247
+ self,
248
+ tag: str,
249
+ schemas: dict[str, IRSchemaObject],
250
+ operations: list
251
+ ) -> GeneratedFile:
252
+ """Generate models.py for a specific app (namespaced structure)."""
253
+ schema_codes = []
254
+
255
+ # Collect enum names used in these schemas
256
+ enum_names = set()
257
+
258
+ # Track seen schema names to avoid duplicates
259
+ seen_schemas = set()
260
+
261
+ # Response models
262
+ for name, schema in schemas.items():
263
+ if schema.is_response_model and name not in seen_schemas:
264
+ schema_codes.append(self.generate_schema(schema))
265
+ seen_schemas.add(name)
266
+ # Collect enums from properties
267
+ if schema.properties:
268
+ for prop in schema.properties.values():
269
+ if prop.enum and prop.name:
270
+ enum_names.add(prop.name.replace(".", ""))
271
+ elif prop.ref and prop.ref in self.context.schemas:
272
+ ref_schema = self.context.schemas[prop.ref]
273
+ if ref_schema.enum:
274
+ enum_names.add(prop.ref.replace(".", ""))
275
+
276
+ # Request models
277
+ for name, schema in schemas.items():
278
+ if schema.is_request_model and name not in seen_schemas:
279
+ schema_codes.append(self.generate_schema(schema))
280
+ seen_schemas.add(name)
281
+ # Collect enums from properties
282
+ if schema.properties:
283
+ for prop in schema.properties.values():
284
+ if prop.enum and prop.name:
285
+ enum_names.add(prop.name.replace(".", ""))
286
+ elif prop.ref and prop.ref in self.context.schemas:
287
+ ref_schema = self.context.schemas[prop.ref]
288
+ if ref_schema.enum:
289
+ enum_names.add(prop.ref.replace(".", ""))
290
+
291
+ # Patch models
292
+ for name, schema in schemas.items():
293
+ if schema.is_patch_model and name not in seen_schemas:
294
+ schema_codes.append(self.generate_schema(schema))
295
+ seen_schemas.add(name)
296
+ # Collect enums from properties
297
+ if schema.properties:
298
+ for prop in schema.properties.values():
299
+ if prop.enum and prop.name:
300
+ enum_names.add(prop.name.replace(".", ""))
301
+ elif prop.ref and prop.ref in self.context.schemas:
302
+ ref_schema = self.context.schemas[prop.ref]
303
+ if ref_schema.enum:
304
+ enum_names.add(prop.ref.replace(".", ""))
305
+
306
+ template = self.jinja_env.get_template('models/app_models.py.jinja')
307
+ content = template.render(
308
+ has_enums=len(enum_names) > 0,
309
+ enum_names=sorted(enum_names), # Sort for consistent output
310
+ schemas=schema_codes
311
+ )
312
+
313
+ folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
314
+ return GeneratedFile(
315
+ path=f"{folder_name}/models.py",
316
+ content=content,
317
+ description=f"Models for {tag}",
318
+ )
@@ -0,0 +1,278 @@
1
+ """
2
+ Operations Generator - Generates operation methods for async and sync clients.
3
+
4
+ Handles:
5
+ - Async operation methods (async def with await)
6
+ - Sync operation methods (def without await)
7
+ - Path parameters, query parameters, request bodies
8
+ - Response parsing and validation
9
+ """
10
+
11
+ from __future__ import annotations
12
+
13
+ from jinja2 import Environment
14
+
15
+ from ...ir import IROperationObject
16
+
17
+
18
+ class OperationsGenerator:
19
+ """Generates operation methods for Python clients."""
20
+
21
+ def __init__(self, jinja_env: Environment, base_generator):
22
+ """
23
+ Initialize operations generator.
24
+
25
+ Args:
26
+ jinja_env: Jinja2 environment for templates
27
+ base_generator: Reference to base generator for utility methods
28
+ """
29
+ self.jinja_env = jinja_env
30
+ self.base = base_generator
31
+
32
+ def generate_async_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
33
+ """Generate async method for operation."""
34
+ # Get method name
35
+ method_name = operation.operation_id
36
+ if remove_tag_prefix and operation.tags:
37
+ # Remove tag prefix using base class method
38
+ tag = operation.tags[0]
39
+ method_name = self.base.remove_tag_prefix(method_name, tag)
40
+
41
+ # Method signature
42
+ params = ["self"]
43
+
44
+ # Add path parameters
45
+ for param in operation.path_parameters:
46
+ param_type = self._map_param_type(param.schema_type)
47
+ params.append(f"{param.name}: {param_type}")
48
+
49
+ # Add request body parameter
50
+ if operation.request_body:
51
+ params.append(f"data: {operation.request_body.schema_name}")
52
+ elif operation.patch_request_body:
53
+ params.append(f"data: {operation.patch_request_body.schema_name} | None = None")
54
+
55
+ # Add query parameters
56
+ for param in operation.query_parameters:
57
+ param_type = self._map_param_type(param.schema_type)
58
+ if not param.required:
59
+ param_type = f"{param_type} | None = None"
60
+ params.append(f"{param.name}: {param_type}")
61
+
62
+ # Return type
63
+ primary_response = operation.primary_success_response
64
+ if primary_response and primary_response.schema_name:
65
+ if operation.is_list_operation:
66
+ return_type = f"list[{primary_response.schema_name}]"
67
+ else:
68
+ return_type = primary_response.schema_name
69
+ else:
70
+ return_type = "None"
71
+
72
+ signature = f"async def {method_name}({', '.join(params)}) -> {return_type}:"
73
+
74
+ # Docstring
75
+ docstring_lines = []
76
+ if operation.summary:
77
+ docstring_lines.append(operation.summary)
78
+ if operation.description:
79
+ if docstring_lines:
80
+ docstring_lines.append("")
81
+ docstring_lines.extend(self.base.wrap_comment(operation.description, 72))
82
+
83
+ docstring = "\n".join(docstring_lines) if docstring_lines else None
84
+
85
+ # Method body
86
+ body_lines = []
87
+
88
+ # Build URL
89
+ url_expr = f'"{operation.path}"'
90
+ if operation.path_parameters:
91
+ # Replace {id} with f-string {id}
92
+ url_expr = f'f"{operation.path}"'
93
+
94
+ body_lines.append(f"url = {url_expr}")
95
+
96
+ # Build request
97
+ request_kwargs = []
98
+
99
+ # Query params
100
+ if operation.query_parameters:
101
+ query_items = []
102
+ for param in operation.query_parameters:
103
+ if param.required:
104
+ query_items.append(f'"{param.name}": {param.name}')
105
+ else:
106
+ query_items.append(f'"{param.name}": {param.name} if {param.name} is not None else None')
107
+
108
+ query_dict = "{" + ", ".join(query_items) + "}"
109
+ request_kwargs.append(f"params={query_dict}")
110
+
111
+ # JSON body
112
+ if operation.request_body:
113
+ # Required body
114
+ request_kwargs.append("json=data.model_dump()")
115
+ elif operation.patch_request_body:
116
+ # Optional PATCH body - check for None
117
+ request_kwargs.append("json=data.model_dump() if data is not None else None")
118
+
119
+ # Make request
120
+ method_lower = operation.http_method.lower()
121
+ request_line = f"response = await self._client.{method_lower}(url"
122
+ if request_kwargs:
123
+ request_line += ", " + ", ".join(request_kwargs)
124
+ request_line += ")"
125
+
126
+ body_lines.append(request_line)
127
+
128
+ # Handle response
129
+ body_lines.append("response.raise_for_status()")
130
+
131
+ if return_type != "None":
132
+ if operation.is_list_operation:
133
+ # Paginated list response - extract results
134
+ body_lines.append(f"data = response.json()")
135
+ body_lines.append(f'return [{primary_response.schema_name}.model_validate(item) for item in data.get("results", [])]')
136
+ else:
137
+ body_lines.append(f"return {primary_response.schema_name}.model_validate(response.json())")
138
+ else:
139
+ body_lines.append("return None")
140
+
141
+ template = self.jinja_env.get_template('client/operation_method.py.jinja')
142
+ return template.render(
143
+ method_name=method_name,
144
+ params=params,
145
+ return_type=return_type,
146
+ docstring=docstring,
147
+ body_lines=body_lines
148
+ )
149
+
150
+ def generate_sync_operation(self, operation: IROperationObject, remove_tag_prefix: bool = False) -> str:
151
+ """Generate sync method for operation (mirrors async generate_operation)."""
152
+ # Get method name
153
+ method_name = operation.operation_id
154
+ if remove_tag_prefix and operation.tags:
155
+ # Remove tag prefix using base class method
156
+ tag = operation.tags[0]
157
+ method_name = self.base.remove_tag_prefix(method_name, tag)
158
+
159
+ # Method signature
160
+ params = ["self"]
161
+
162
+ # Add path parameters
163
+ for param in operation.path_parameters:
164
+ param_type = self._map_param_type(param.schema_type)
165
+ params.append(f"{param.name}: {param_type}")
166
+
167
+ # Add request body parameter
168
+ if operation.request_body:
169
+ params.append(f"data: {operation.request_body.schema_name}")
170
+ elif operation.patch_request_body:
171
+ params.append(f"data: {operation.patch_request_body.schema_name} | None = None")
172
+
173
+ # Add query parameters
174
+ for param in operation.query_parameters:
175
+ param_type = self._map_param_type(param.schema_type)
176
+ if not param.required:
177
+ param_type = f"{param_type} | None = None"
178
+ params.append(f"{param.name}: {param_type}")
179
+
180
+ # Return type
181
+ primary_response = operation.primary_success_response
182
+ if primary_response and primary_response.schema_name:
183
+ if operation.is_list_operation:
184
+ return_type = f"list[{primary_response.schema_name}]"
185
+ else:
186
+ return_type = primary_response.schema_name
187
+ else:
188
+ return_type = "None"
189
+
190
+ # Docstring
191
+ docstring_lines = []
192
+ if operation.summary:
193
+ docstring_lines.append(operation.summary)
194
+ if operation.description:
195
+ if docstring_lines:
196
+ docstring_lines.append("")
197
+ docstring_lines.extend(self.base.wrap_comment(operation.description, 72))
198
+
199
+ docstring = "\n".join(docstring_lines) if docstring_lines else None
200
+
201
+ # Method body
202
+ body_lines = []
203
+
204
+ # Build URL
205
+ url_expr = f'"{operation.path}"'
206
+ if operation.path_parameters:
207
+ # Replace {id} with f-string {id}
208
+ url_expr = f'f"{operation.path}"'
209
+
210
+ body_lines.append(f"url = {url_expr}")
211
+
212
+ # Build request
213
+ request_kwargs = []
214
+
215
+ # Query params
216
+ if operation.query_parameters:
217
+ query_items = []
218
+ for param in operation.query_parameters:
219
+ if param.required:
220
+ query_items.append(f'"{param.name}": {param.name}')
221
+ else:
222
+ query_items.append(f'"{param.name}": {param.name} if {param.name} is not None else None')
223
+
224
+ query_dict = "{" + ", ".join(query_items) + "}"
225
+ request_kwargs.append(f"params={query_dict}")
226
+
227
+ # JSON body
228
+ if operation.request_body:
229
+ # Required body
230
+ request_kwargs.append("json=data.model_dump(exclude_unset=True)")
231
+ elif operation.patch_request_body:
232
+ # Optional PATCH body - check for None
233
+ request_kwargs.append("json=data.model_dump(exclude_unset=True) if data is not None else None")
234
+
235
+ # HTTP method
236
+ method_lower = operation.http_method.lower()
237
+
238
+ # Build request call (sync version - no await)
239
+ if request_kwargs:
240
+ request_call = f'self._client.{method_lower}(url, {", ".join(request_kwargs)})'
241
+ else:
242
+ request_call = f'self._client.{method_lower}(url)'
243
+
244
+ body_lines.append(f"response = {request_call}")
245
+ body_lines.append("response.raise_for_status()")
246
+
247
+ # Parse response
248
+ if return_type != "None":
249
+ if operation.is_list_operation:
250
+ # List response - validate each item (paginated)
251
+ primary_schema = primary_response.schema_name
252
+ body_lines.append(f"data = response.json()")
253
+ body_lines.append(f'return [{primary_schema}.model_validate(item) for item in data.get("results", [])]')
254
+ else:
255
+ # Single object response
256
+ body_lines.append(f"return {return_type}.model_validate(response.json())")
257
+
258
+ # Render template
259
+ template = self.jinja_env.get_template('client/sync_operation_method.py.jinja')
260
+ return template.render(
261
+ method_name=method_name,
262
+ params=params,
263
+ return_type=return_type,
264
+ body_lines=body_lines,
265
+ docstring=docstring
266
+ )
267
+
268
+ def _map_param_type(self, schema_type: str) -> str:
269
+ """Map parameter schema type to Python type."""
270
+ type_map = {
271
+ "string": "str",
272
+ "integer": "int",
273
+ "number": "float",
274
+ "boolean": "bool",
275
+ "array": "list",
276
+ "object": "dict",
277
+ }
278
+ return type_map.get(schema_type, "str")
@@ -0,0 +1,102 @@
1
+ """
2
+ Sync Client Generator - Generates sync Python clients.
3
+
4
+ Handles:
5
+ - Main SyncAPIClient class (httpx.Client)
6
+ - Sync sub-client classes (per tag/app)
7
+ - Mirrors async client structure without async/await
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from jinja2 import Environment
13
+
14
+ from ..base import GeneratedFile
15
+ from ...ir import IROperationObject
16
+
17
+
18
+ class SyncClientGenerator:
19
+ """Generates sync Python client files."""
20
+
21
+ def __init__(self, jinja_env: Environment, base_generator, operations_gen):
22
+ """
23
+ Initialize sync client generator.
24
+
25
+ Args:
26
+ jinja_env: Jinja2 environment for templates
27
+ base_generator: Reference to base generator
28
+ operations_gen: Operations generator instance
29
+ """
30
+ self.jinja_env = jinja_env
31
+ self.base = base_generator
32
+ self.operations_gen = operations_gen
33
+
34
+ def generate_app_sync_client_file(self, tag: str, operations: list[IROperationObject]) -> GeneratedFile:
35
+ """Generate sync_client.py for a specific app."""
36
+ class_name = self.base.tag_to_class_name(tag)
37
+
38
+ # Generate sync methods
39
+ method_codes = []
40
+ for operation in operations:
41
+ method_codes.append(self.operations_gen.generate_sync_operation(operation, remove_tag_prefix=True))
42
+
43
+ template = self.jinja_env.get_template('client/sync_sub_client.py.jinja')
44
+ content = template.render(
45
+ tag=self.base.tag_to_display_name(tag),
46
+ class_name=class_name,
47
+ operations=method_codes
48
+ )
49
+
50
+ folder_name = self.base.tag_and_app_to_folder_name(tag, operations)
51
+ return GeneratedFile(
52
+ path=f"{folder_name}/sync_client.py",
53
+ content=content,
54
+ description=f"Sync API client for {tag}",
55
+ )
56
+
57
+ def generate_sync_main_client_file(self, ops_by_tag: dict) -> GeneratedFile:
58
+ """Generate sync_client.py with SyncAPIClient."""
59
+ tags = sorted(ops_by_tag.keys())
60
+
61
+ # Prepare tags data for template (with Sync prefix for imports)
62
+ tags_data = [
63
+ {
64
+ "class_name": f"Sync{self.base.tag_to_class_name(tag)}", # Add Sync prefix
65
+ "slug": f"{self.base.tag_and_app_to_folder_name(tag, ops_by_tag[tag])}.sync_client", # Import from sync_client module
66
+ }
67
+ for tag in tags
68
+ ]
69
+
70
+ # Generate sync APIClient class
71
+ sync_client_code = self._generate_sync_main_client_class(ops_by_tag)
72
+
73
+ template = self.jinja_env.get_template('client/main_client_file.py.jinja')
74
+ content = template.render(
75
+ tags=tags_data,
76
+ client_code=sync_client_code
77
+ )
78
+
79
+ return GeneratedFile(
80
+ path="sync_client.py",
81
+ content=content,
82
+ description="Main sync API client",
83
+ )
84
+
85
+ def _generate_sync_main_client_class(self, ops_by_tag: dict) -> str:
86
+ """Generate main SyncAPIClient with sync sub-clients."""
87
+ tags = sorted(ops_by_tag.keys())
88
+
89
+ # Prepare tags data for template
90
+ tags_data = [
91
+ {
92
+ "class_name": self.base.tag_to_class_name(tag),
93
+ "property": self.base.tag_to_property_name(tag),
94
+ }
95
+ for tag in tags
96
+ ]
97
+
98
+ template = self.jinja_env.get_template('client/sync_main_client.py.jinja')
99
+ return template.render(
100
+ api_title=self.base.context.openapi_info.title,
101
+ tags=tags_data
102
+ )