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,838 @@
1
+ """
2
+ Base Generator - Common code generation logic.
3
+
4
+ This module defines the abstract BaseGenerator class that provides
5
+ common functionality for all code generators (Python, TypeScript, etc.).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from abc import ABC, abstractmethod
11
+ from pathlib import Path
12
+ from typing import Any
13
+
14
+ from ..ir import IRContext, IROperationObject, IRSchemaObject
15
+
16
+
17
+ class GeneratedFile:
18
+ """
19
+ Represents a generated file.
20
+
21
+ Attributes:
22
+ path: Relative file path (e.g., 'models.py', 'client.ts')
23
+ content: Generated file content
24
+ description: Human-readable description
25
+ """
26
+
27
+ def __init__(self, path: str, content: str, description: str | None = None):
28
+ self.path = path
29
+ self.content = content
30
+ self.description = description
31
+
32
+ def __repr__(self) -> str:
33
+ return f"GeneratedFile(path={self.path!r}, size={len(self.content)} bytes)"
34
+
35
+
36
+ class BaseGenerator(ABC):
37
+ """
38
+ Abstract base generator for IR → Code conversion.
39
+
40
+ Subclasses implement language-specific generation:
41
+ - PythonGenerator: Generates Python client (Pydantic 2 + httpx)
42
+ - TypeScriptGenerator: Generates TypeScript client (Fetch API)
43
+ """
44
+
45
+ def __init__(
46
+ self,
47
+ context: IRContext,
48
+ client_structure: str = "namespaced",
49
+ openapi_schema: dict | None = None,
50
+ tag_prefix: str = "",
51
+ package_config: dict | None = None,
52
+ generate_package_files: bool = False,
53
+ generate_zod_schemas: bool = False,
54
+ generate_fetchers: bool = False,
55
+ generate_swr_hooks: bool = False,
56
+ ):
57
+ """
58
+ Initialize generator with IR context.
59
+
60
+ Args:
61
+ context: IRContext from parser
62
+ client_structure: Client structure ("flat" or "namespaced")
63
+ openapi_schema: OpenAPI schema dict (for embedding in client)
64
+ tag_prefix: Prefix to add to all tag names (e.g., "cfg_")
65
+ package_config: Package configuration (name, version, author, etc.)
66
+ generate_package_files: Whether to generate package.json/pyproject.toml
67
+ generate_zod_schemas: Whether to generate Zod schemas (TypeScript only)
68
+ generate_fetchers: Whether to generate typed fetchers (TypeScript only)
69
+ generate_swr_hooks: Whether to generate SWR hooks (TypeScript only, React)
70
+ """
71
+ self.context = context
72
+ self.client_structure = client_structure
73
+ self.openapi_schema = openapi_schema
74
+ self.tag_prefix = tag_prefix
75
+ self.package_config = package_config or {}
76
+ self.generate_package_files = generate_package_files
77
+ self.generate_zod_schemas = generate_zod_schemas
78
+ self.generate_fetchers = generate_fetchers
79
+ self.generate_swr_hooks = generate_swr_hooks
80
+
81
+ # ===== Namespaced Structure Helpers =====
82
+
83
+ def group_operations_by_tag(self) -> dict[str, list[IROperationObject]]:
84
+ """
85
+ Group operations by their first tag with case-insensitive normalization.
86
+
87
+ Tags are normalized to prevent duplicates caused by case differences
88
+ (e.g., "Profiles" and "profiles" are treated as the same tag).
89
+ The canonical tag (first encountered) is preserved for display purposes.
90
+
91
+ Returns:
92
+ Dictionary mapping canonical tag names to lists of operations.
93
+ Operations without tags are grouped under "default".
94
+
95
+ Examples:
96
+ >>> ops = generator.group_operations_by_tag()
97
+ >>> ops["users"] # [list_op, create_op, retrieve_op, ...]
98
+ >>> # "Users" and "users" are merged into one group with canonical "Users"
99
+ """
100
+ from collections import defaultdict
101
+
102
+ ops_by_tag = defaultdict(list)
103
+ tag_canonical = {} # normalized_tag -> canonical_tag mapping
104
+ tag_variants = defaultdict(set) # Track all variants for warnings
105
+
106
+ for op_id, operation in self.context.operations.items():
107
+ tag = operation.tags[0] if operation.tags else "default"
108
+
109
+ # Normalize tag to lowercase for comparison
110
+ normalized_tag = tag.lower()
111
+
112
+ # Track all case variants
113
+ tag_variants[normalized_tag].add(tag)
114
+
115
+ # Use first encountered version as canonical
116
+ if normalized_tag not in tag_canonical:
117
+ tag_canonical[normalized_tag] = tag
118
+
119
+ # Group under canonical tag
120
+ canonical = tag_canonical[normalized_tag]
121
+ ops_by_tag[canonical].append(operation)
122
+
123
+ # Warn about case inconsistencies
124
+ for normalized, variants in tag_variants.items():
125
+ if len(variants) > 1:
126
+ canonical = tag_canonical[normalized]
127
+ other_variants = sorted(variants - {canonical})
128
+ print(f"⚠️ Warning: Found case variants of tag '{canonical}': {other_variants}")
129
+ print(f" → Using '{canonical}' as canonical tag")
130
+
131
+ return dict(ops_by_tag)
132
+
133
+ def tag_to_class_name(self, tag: str, suffix: str = "API") -> str:
134
+ """
135
+ Convert tag to PascalCase class name.
136
+
137
+ Args:
138
+ tag: Tag name (e.g., "users", "user-management", "django_cfg.auth")
139
+ suffix: Class name suffix (default: "API")
140
+
141
+ Returns:
142
+ PascalCase class name with suffix and optional prefix
143
+
144
+ Examples:
145
+ >>> generator.tag_to_class_name("users")
146
+ 'UsersAPI'
147
+ >>> generator.tag_to_class_name("user-management")
148
+ 'UserManagementAPI'
149
+ >>> generator.tag_to_class_name("django_cfg.auth")
150
+ 'DjangoCfgAuthAPI'
151
+ >>> generator = BaseGenerator(context, tag_prefix="cfg_")
152
+ >>> generator.tag_to_class_name("auth")
153
+ 'CfgAuthAPI'
154
+ """
155
+ # Use tag_to_property_name to get normalized name with prefix
156
+ normalized = self.tag_to_property_name(tag)
157
+ # Split into words and capitalize
158
+ words = normalized.split('_')
159
+ return ''.join(word.capitalize() for word in words if word) + suffix
160
+
161
+ def tag_to_property_name(self, tag: str) -> str:
162
+ """
163
+ Convert tag to valid property/variable name.
164
+
165
+ Args:
166
+ tag: Tag name (e.g., "Blog - Categories", "user-management", "django_cfg.leads")
167
+
168
+ Returns:
169
+ Valid identifier (snake_case for Python, camelCase for TS) with optional prefix
170
+
171
+ Examples:
172
+ >>> generator.tag_to_property_name("Blog - Categories")
173
+ 'blog_categories'
174
+ >>> generator.tag_to_property_name("user-management")
175
+ 'user_management'
176
+ >>> generator = BaseGenerator(context, tag_prefix="cfg_")
177
+ >>> generator.tag_to_property_name("auth")
178
+ 'cfg_auth'
179
+ >>> generator.tag_to_property_name("django_cfg.leads")
180
+ 'cfg_leads' # django_cfg prefix is stripped before adding group prefix
181
+ """
182
+ from django.utils.text import slugify
183
+ normalized = slugify(tag).replace('-', '_')
184
+
185
+ # Strip common app label prefixes to avoid duplication
186
+ # (e.g., "django_cfg_leads" → "leads" before adding group prefix)
187
+ prefixes_to_strip = ['django_cfg_', 'django_cfg.']
188
+ for prefix in prefixes_to_strip:
189
+ prefix_normalized = slugify(prefix).replace('-', '_')
190
+ if normalized.startswith(prefix_normalized):
191
+ normalized = normalized[len(prefix_normalized):]
192
+ break
193
+
194
+ # Strip leading underscores after prefix removal
195
+ # (e.g., "django_cfg.accounts" → "django_cfg_accounts" → "_accounts" → "accounts")
196
+ normalized = normalized.lstrip('_')
197
+
198
+ # Add group prefix if configured and not already present
199
+ if self.tag_prefix:
200
+ # Check if tag already starts with prefix (avoid duplication)
201
+ if not normalized.startswith(self.tag_prefix):
202
+ normalized = f"{self.tag_prefix}{normalized}"
203
+
204
+ return normalized
205
+
206
+ def extract_app_from_path(self, path: str) -> str | None:
207
+ """
208
+ Extract Django app name from URL path.
209
+
210
+ Args:
211
+ path: URL path (e.g., "/django_cfg_leads/leads/", "/django_cfg_newsletter/campaigns/")
212
+
213
+ Returns:
214
+ App name without trailing slash, or None if no app detected
215
+
216
+ Examples:
217
+ >>> generator.extract_app_from_path("/django_cfg_leads/leads/")
218
+ 'django_cfg_leads'
219
+ >>> generator.extract_app_from_path("/django_cfg_newsletter/campaigns/")
220
+ 'django_cfg_newsletter'
221
+ >>> generator.extract_app_from_path("/api/users/")
222
+ 'api'
223
+ """
224
+ # Remove leading/trailing slashes and split
225
+ parts = path.strip('/').split('/')
226
+
227
+ # First part is usually the app name
228
+ if parts:
229
+ return parts[0]
230
+
231
+ return None
232
+
233
+ def tag_and_app_to_folder_name(self, tag: str, operations: list) -> str:
234
+ """
235
+ Generate folder name from tag and app name with smart deduplication.
236
+
237
+ When tag matches app name, the redundant tag portion is omitted to avoid
238
+ folder names like 'cfg__tasks__tasks'. This keeps naming clean and intuitive.
239
+
240
+ Args:
241
+ tag: Tag name (e.g., "Campaigns", "Tasks", "django_cfg.leads")
242
+ operations: List of operations to extract app name from
243
+
244
+ Returns:
245
+ Folder name in one of these formats:
246
+ - group__app__tag (when tag differs from app)
247
+ - group__app (when tag matches app)
248
+ - group (when all three match - rare case)
249
+
250
+ Examples:
251
+ >>> # Distinct tag and app
252
+ >>> # operations with path="/django_cfg_newsletter/campaigns/"
253
+ >>> generator.tag_and_app_to_folder_name("Campaigns", operations)
254
+ 'cfg__newsletter__campaigns'
255
+
256
+ >>> # Tag matches app (deduplication applied)
257
+ >>> # operations with path="/tasks/api/..."
258
+ >>> generator.tag_and_app_to_folder_name("Tasks", operations)
259
+ 'cfg__tasks'
260
+
261
+ >>> # Tag matches app (deduplication applied)
262
+ >>> # operations with path="/django_cfg_leads/leads/"
263
+ >>> generator.tag_and_app_to_folder_name("Leads", operations)
264
+ 'cfg__leads'
265
+
266
+ >>> # Triple match - all same (rare)
267
+ >>> # operations with path="/profiles/profiles/"
268
+ >>> generator.tag_and_app_to_folder_name("Profiles", operations) # group=profiles
269
+ 'profiles'
270
+ """
271
+ from django.utils.text import slugify
272
+
273
+ # Extract app name from first operation's path
274
+ app_name = None
275
+ if operations:
276
+ app_name = self.extract_app_from_path(operations[0].path)
277
+
278
+ if not app_name:
279
+ # Fallback: just use normalized tag
280
+ return self.tag_to_property_name(tag)
281
+
282
+ # Normalize app name (strip django_cfg prefix)
283
+ normalized_app = slugify(app_name).replace('-', '_')
284
+ prefixes_to_strip = ['django_cfg_', 'django_cfg.']
285
+ for prefix in prefixes_to_strip:
286
+ prefix_normalized = slugify(prefix).replace('-', '_')
287
+ if normalized_app.startswith(prefix_normalized):
288
+ normalized_app = normalized_app[len(prefix_normalized):]
289
+ break
290
+ normalized_app = normalized_app.lstrip('_')
291
+
292
+ # Normalize tag (strip django_cfg prefix)
293
+ normalized_tag = slugify(tag).replace('-', '_')
294
+ for prefix in prefixes_to_strip:
295
+ prefix_normalized = slugify(prefix).replace('-', '_')
296
+ if normalized_tag.startswith(prefix_normalized):
297
+ normalized_tag = normalized_tag[len(prefix_normalized):]
298
+ break
299
+ normalized_tag = normalized_tag.lstrip('_')
300
+
301
+ # Smart deduplication: if tag matches app, skip tag portion
302
+ if normalized_tag == normalized_app:
303
+ # Tag is redundant with app name
304
+ if self.tag_prefix:
305
+ group = self.tag_prefix.rstrip('_')
306
+ # Check if group also matches app (triple redundancy)
307
+ if group == normalized_app:
308
+ # All three are the same, just use group
309
+ return group
310
+ else:
311
+ # Tag matches app but not group: use group__app
312
+ return f"{group}__{normalized_app}"
313
+ else:
314
+ # No group prefix, just use app name
315
+ return normalized_app
316
+
317
+ # Build folder name: group__app__tag (when tag is distinct)
318
+ if self.tag_prefix:
319
+ # tag_prefix already has trailing underscore: "cfg_"
320
+ group = self.tag_prefix.rstrip('_')
321
+ return f"{group}__{normalized_app}__{normalized_tag}"
322
+ else:
323
+ return f"{normalized_app}__{normalized_tag}"
324
+
325
+ def tag_to_display_name(self, tag: str) -> str:
326
+ """
327
+ Convert tag to human-readable display name for docstrings.
328
+
329
+ This method strips common prefixes and formats the tag for display in
330
+ documentation without adding group prefixes.
331
+
332
+ Args:
333
+ tag: Tag name (e.g., "django_cfg.leads", "Campaigns", "User Profile")
334
+
335
+ Returns:
336
+ Human-readable tag name (title case)
337
+
338
+ Examples:
339
+ >>> generator.tag_to_display_name("Campaigns")
340
+ 'Campaigns'
341
+ >>> generator.tag_to_display_name("django_cfg.leads")
342
+ 'Leads'
343
+ >>> generator.tag_to_display_name("django_cfg_accounts")
344
+ 'Accounts'
345
+ >>> generator.tag_to_display_name("user-management")
346
+ 'User Management'
347
+ """
348
+ from django.utils.text import slugify
349
+
350
+ # If tag is already in title case with spaces, return as-is
351
+ if ' ' in tag and tag[0].isupper():
352
+ return tag
353
+
354
+ # Normalize the tag
355
+ normalized = slugify(tag).replace('-', '_')
356
+
357
+ # Strip common app label prefixes
358
+ prefixes_to_strip = ['django_cfg_', 'django_cfg.']
359
+ for prefix in prefixes_to_strip:
360
+ prefix_normalized = slugify(prefix).replace('-', '_')
361
+ if normalized.startswith(prefix_normalized):
362
+ normalized = normalized[len(prefix_normalized):]
363
+ break
364
+
365
+ # Strip leading underscores
366
+ normalized = normalized.lstrip('_')
367
+
368
+ # Convert to title case with spaces
369
+ words = normalized.split('_')
370
+ return ' '.join(word.capitalize() for word in words if word)
371
+
372
+ def remove_tag_prefix(self, operation_id: str, tag: str) -> str:
373
+ """
374
+ Remove tag prefix from operation_id.
375
+
376
+ This method handles complex operation ID patterns by:
377
+ 1. Stripping common app label prefixes (django_cfg_*, etc.)
378
+ 2. Attempting to strip the normalized tag name
379
+ 3. Returning the cleaned operation_id
380
+
381
+ Args:
382
+ operation_id: Operation ID (e.g., "django_cfg_newsletter_campaigns_list", "posts_list")
383
+ tag: Tag name (e.g., "Campaigns", "posts", "django_cfg.accounts")
384
+
385
+ Returns:
386
+ Operation ID without tag prefix
387
+
388
+ Examples:
389
+ >>> generator.remove_tag_prefix("posts_list", "posts")
390
+ 'list'
391
+ >>> generator.remove_tag_prefix("django_cfg_newsletter_campaigns_list", "Campaigns")
392
+ 'list'
393
+ >>> generator.remove_tag_prefix("django_cfg_accounts_token_refresh_create", "Auth")
394
+ 'token_refresh_create'
395
+ >>> generator.remove_tag_prefix("retrieve", "users")
396
+ 'retrieve' # No prefix to remove
397
+ """
398
+ from django.utils.text import slugify
399
+
400
+ # First, strip common app label prefixes from operation_id
401
+ # This handles cases like "django_cfg_newsletter_campaigns_list"
402
+ cleaned_op_id = operation_id
403
+ app_prefixes_to_strip = [
404
+ 'django_cfg_newsletter_',
405
+ 'django_cfg_accounts_',
406
+ 'django_cfg_leads_',
407
+ 'django_cfg_support_',
408
+ 'django_cfg_agents_',
409
+ 'django_cfg_knowbase_',
410
+ 'django_cfg_payments_',
411
+ 'django_cfg_tasks_',
412
+ 'django_cfg_', # Catch-all for any django_cfg_* prefixes
413
+ ]
414
+
415
+ for app_prefix in app_prefixes_to_strip:
416
+ if cleaned_op_id.startswith(app_prefix):
417
+ cleaned_op_id = cleaned_op_id[len(app_prefix):]
418
+ break
419
+
420
+ # Now try to remove the normalized tag as a prefix
421
+ # Normalize tag same way as tag_to_property_name but without adding group prefix
422
+ normalized_tag = slugify(tag).replace('-', '_')
423
+
424
+ # Strip django_cfg prefix from tag too
425
+ tag_prefixes = ['django_cfg_', 'django_cfg.']
426
+ for prefix in tag_prefixes:
427
+ prefix_normalized = slugify(prefix).replace('-', '_')
428
+ if normalized_tag.startswith(prefix_normalized):
429
+ normalized_tag = normalized_tag[len(prefix_normalized):]
430
+ break
431
+
432
+ # Strip leading underscores from tag
433
+ normalized_tag = normalized_tag.lstrip('_')
434
+
435
+ # Try to remove normalized tag as prefix
436
+ tag_prefix = f"{normalized_tag}_"
437
+ if cleaned_op_id.startswith(tag_prefix):
438
+ cleaned_op_id = cleaned_op_id[len(tag_prefix):]
439
+
440
+ return cleaned_op_id
441
+
442
+ # ===== Main Generate Method =====
443
+
444
+ @abstractmethod
445
+ def generate(self) -> list[GeneratedFile]:
446
+ """
447
+ Generate all client files.
448
+
449
+ Returns:
450
+ List of GeneratedFile objects
451
+
452
+ Examples:
453
+ >>> generator = PythonGenerator(context)
454
+ >>> files = generator.generate()
455
+ >>> for file in files:
456
+ ... print(f"{file.path}: {len(file.content)} bytes")
457
+ models.py: 1234 bytes
458
+ client.py: 5678 bytes
459
+ """
460
+ pass
461
+
462
+ # ===== Schema Generation (Abstract) =====
463
+
464
+ @abstractmethod
465
+ def generate_schema(self, schema: IRSchemaObject) -> str:
466
+ """
467
+ Generate code for a single schema.
468
+
469
+ Args:
470
+ schema: IRSchemaObject to generate
471
+
472
+ Returns:
473
+ Generated code (class definition, interface, etc.)
474
+
475
+ Examples:
476
+ >>> schema = IRSchemaObject(name="User", type="object", ...)
477
+ >>> code = generator.generate_schema(schema)
478
+ >>> # Python: "class User(BaseModel): ..."
479
+ >>> # TypeScript: "interface User { ... }"
480
+ """
481
+ pass
482
+
483
+ @abstractmethod
484
+ def generate_enum(self, schema: IRSchemaObject) -> str:
485
+ """
486
+ Generate enum code from schema with x-enum-varnames.
487
+
488
+ Args:
489
+ schema: IRSchemaObject with enum + enum_var_names
490
+
491
+ Returns:
492
+ Generated enum code
493
+
494
+ Examples:
495
+ >>> schema = IRSchemaObject(
496
+ ... name="StatusEnum",
497
+ ... type="integer",
498
+ ... enum=[1, 2, 3],
499
+ ... enum_var_names=["STATUS_NEW", "STATUS_IN_PROGRESS", "STATUS_COMPLETE"]
500
+ ... )
501
+ >>> code = generator.generate_enum(schema)
502
+ >>> # Python: "class StatusEnum(IntEnum): ..."
503
+ >>> # TypeScript: "enum StatusEnum { ... }"
504
+ """
505
+ pass
506
+
507
+ # ===== Operation Generation (Abstract) =====
508
+
509
+ @abstractmethod
510
+ def generate_operation(self, operation: IROperationObject) -> str:
511
+ """
512
+ Generate code for a single operation (endpoint method).
513
+
514
+ Args:
515
+ operation: IROperationObject to generate
516
+
517
+ Returns:
518
+ Generated method code
519
+
520
+ Examples:
521
+ >>> operation = IROperationObject(
522
+ ... operation_id="users_list",
523
+ ... http_method="GET",
524
+ ... path="/api/users/",
525
+ ... ...
526
+ ... )
527
+ >>> code = generator.generate_operation(operation)
528
+ >>> # Python: "async def users_list(self, ...) -> list[User]: ..."
529
+ >>> # TypeScript: "async usersList(...): Promise<User[]> { ... }"
530
+ """
531
+ pass
532
+
533
+ # ===== Helpers =====
534
+
535
+ def get_request_schemas(self) -> dict[str, IRSchemaObject]:
536
+ """Get all request schemas (UserRequest, etc.)."""
537
+ return self.context.request_models
538
+
539
+ def get_response_schemas(self) -> dict[str, IRSchemaObject]:
540
+ """Get all response schemas (User, etc.)."""
541
+ return self.context.response_models
542
+
543
+ def get_patch_schemas(self) -> dict[str, IRSchemaObject]:
544
+ """Get all PATCH schemas (PatchedUser, etc.)."""
545
+ return self.context.patch_models
546
+
547
+ def get_enum_schemas(self) -> dict[str, IRSchemaObject]:
548
+ """Get all enum schemas with x-enum-varnames."""
549
+ return self.context.enum_schemas
550
+
551
+ def _collect_enums_from_schemas(
552
+ self, schemas: dict[str, IRSchemaObject]
553
+ ) -> dict[str, IRSchemaObject]:
554
+ """
555
+ Recursively collect all enum schemas used by given schemas.
556
+
557
+ This method extracts enums from:
558
+ - Top-level enum schemas (StatusEnum as a component)
559
+ - Nested enum properties (User.status where status has enum)
560
+ - Array items with enums (Task.tags where tags[0] is enum)
561
+
562
+ Auto-generates enum_var_names if not present:
563
+ - "open" → "OPEN"
564
+ - "waiting_for_user" → "WAITING_FOR_USER"
565
+
566
+ Args:
567
+ schemas: Dictionary of schemas to scan
568
+
569
+ Returns:
570
+ Dictionary of enum schemas found (with auto-generated var names)
571
+
572
+ Examples:
573
+ >>> schemas = {
574
+ ... "Payment": IRSchemaObject(
575
+ ... name="Payment",
576
+ ... type="object",
577
+ ... properties={
578
+ ... "status": IRSchemaObject(
579
+ ... name="StatusEnum",
580
+ ... type="string",
581
+ ... enum=["open", "closed"]
582
+ ... )
583
+ ... }
584
+ ... )
585
+ ... }
586
+ >>> enums = generator._collect_enums_from_schemas(schemas)
587
+ >>> enums["StatusEnum"].enum_var_names
588
+ ["OPEN", "CLOSED"]
589
+ """
590
+ enums = {}
591
+
592
+ def auto_generate_enum_var_names(schema: IRSchemaObject) -> IRSchemaObject:
593
+ """Auto-generate enum_var_names from enum values if missing."""
594
+ if schema.enum and not schema.enum_var_names:
595
+ # Generate variable names from values
596
+ var_names = []
597
+ for value in schema.enum:
598
+ if isinstance(value, str):
599
+ # Convert "waiting_for_user" → "WAITING_FOR_USER"
600
+ var_name = value.upper().replace("-", "_").replace(" ", "_")
601
+ else:
602
+ # For integers: 1 → "VALUE_1"
603
+ var_name = f"VALUE_{value}"
604
+ var_names.append(var_name)
605
+
606
+ # Create new schema with auto-generated var names
607
+ schema = IRSchemaObject(
608
+ **{**schema.model_dump(), "enum_var_names": var_names}
609
+ )
610
+ return schema
611
+
612
+ def collect_recursive(schema: IRSchemaObject):
613
+ """Recursively collect enums from schema and its nested properties."""
614
+ # Check if this schema itself is an enum (with or without x-enum-varnames)
615
+ if schema.enum and schema.name:
616
+ schema = auto_generate_enum_var_names(schema)
617
+ enums[schema.name] = schema
618
+
619
+ # Check if this schema is a reference to an enum
620
+ if schema.ref and schema.ref in self.context.schemas:
621
+ ref_schema = self.context.schemas[schema.ref]
622
+ if ref_schema.enum:
623
+ ref_schema = auto_generate_enum_var_names(ref_schema)
624
+ enums[ref_schema.name] = ref_schema
625
+
626
+ # Check properties for enums
627
+ if schema.properties:
628
+ for prop_schema in schema.properties.values():
629
+ # If property has enum, it's a standalone enum
630
+ if prop_schema.enum and prop_schema.name:
631
+ prop_schema = auto_generate_enum_var_names(prop_schema)
632
+ enums[prop_schema.name] = prop_schema
633
+ # Check if property is a reference to an enum
634
+ elif prop_schema.ref and prop_schema.ref in self.context.schemas:
635
+ ref_schema = self.context.schemas[prop_schema.ref]
636
+ if ref_schema.enum:
637
+ ref_schema = auto_generate_enum_var_names(ref_schema)
638
+ enums[ref_schema.name] = ref_schema
639
+ # Recurse into nested objects
640
+ elif prop_schema.type == "object":
641
+ collect_recursive(prop_schema)
642
+ # Recurse into arrays
643
+ elif prop_schema.type == "array" and prop_schema.items:
644
+ if prop_schema.items.enum and prop_schema.items.name:
645
+ items = auto_generate_enum_var_names(prop_schema.items)
646
+ enums[items.name] = items
647
+ elif prop_schema.items.ref and prop_schema.items.ref in self.context.schemas:
648
+ ref_items = self.context.schemas[prop_schema.items.ref]
649
+ if ref_items.enum:
650
+ ref_items = auto_generate_enum_var_names(ref_items)
651
+ enums[ref_items.name] = ref_items
652
+ elif prop_schema.items.type == "object":
653
+ collect_recursive(prop_schema.items)
654
+
655
+ # Check array items for enums (if schema itself is array)
656
+ if schema.items:
657
+ if schema.items.enum and schema.items.name:
658
+ items = auto_generate_enum_var_names(schema.items)
659
+ enums[items.name] = items
660
+ elif schema.items.ref and schema.items.ref in self.context.schemas:
661
+ ref_items = self.context.schemas[schema.items.ref]
662
+ if ref_items.enum:
663
+ ref_items = auto_generate_enum_var_names(ref_items)
664
+ enums[ref_items.name] = ref_items
665
+ elif schema.items.type == "object":
666
+ collect_recursive(schema.items)
667
+
668
+ # Collect enums from all schemas
669
+ for schema in schemas.values():
670
+ collect_recursive(schema)
671
+
672
+ return enums
673
+
674
+ def get_operations_by_tag(self) -> dict[str, list[IROperationObject]]:
675
+ """Get operations grouped by tags."""
676
+ return self.context.operations_by_tag
677
+
678
+ def save_files(self, files: list[GeneratedFile], output_dir: Path) -> None:
679
+ """
680
+ Save generated files to disk.
681
+
682
+ Args:
683
+ files: List of GeneratedFile objects
684
+ output_dir: Output directory path
685
+
686
+ Examples:
687
+ >>> files = generator.generate()
688
+ >>> generator.save_files(files, Path("./generated"))
689
+ """
690
+ output_dir.mkdir(parents=True, exist_ok=True)
691
+
692
+ for file in files:
693
+ file_path = output_dir / file.path
694
+ file_path.parent.mkdir(parents=True, exist_ok=True)
695
+
696
+ with open(file_path, "w", encoding="utf-8") as f:
697
+ f.write(file.content)
698
+
699
+ # ===== Formatting Helpers =====
700
+
701
+ def indent(self, code: str, spaces: int = 4) -> str:
702
+ """
703
+ Indent code by N spaces.
704
+
705
+ Args:
706
+ code: Code to indent
707
+ spaces: Number of spaces (default: 4)
708
+
709
+ Returns:
710
+ Indented code
711
+
712
+ Examples:
713
+ >>> generator.indent("x = 1\\ny = 2", 4)
714
+ ' x = 1\\n y = 2'
715
+ """
716
+ lines = code.split("\n")
717
+ indent_str = " " * spaces
718
+ return "\n".join(f"{indent_str}{line}" if line.strip() else line for line in lines)
719
+
720
+ def sanitize_name(self, name: str) -> str:
721
+ """
722
+ Sanitize schema/operation name to valid identifier.
723
+
724
+ Args:
725
+ name: Raw name
726
+
727
+ Returns:
728
+ Sanitized name
729
+
730
+ Examples:
731
+ >>> generator.sanitize_name("User-Profile")
732
+ 'User_Profile'
733
+ >>> generator.sanitize_name("2Users")
734
+ '_2Users'
735
+ """
736
+ # Replace invalid characters with underscore
737
+ sanitized = "".join(c if c.isalnum() or c == "_" else "_" for c in name)
738
+
739
+ # Ensure doesn't start with digit
740
+ if sanitized and sanitized[0].isdigit():
741
+ sanitized = f"_{sanitized}"
742
+
743
+ return sanitized or "Unknown"
744
+
745
+ def wrap_comment(self, text: str, max_length: int = 80) -> list[str]:
746
+ """
747
+ Wrap long comment text to multiple lines.
748
+
749
+ Args:
750
+ text: Comment text
751
+ max_length: Maximum line length
752
+
753
+ Returns:
754
+ List of wrapped lines
755
+
756
+ Examples:
757
+ >>> generator.wrap_comment("This is a very long comment that should be wrapped", 30)
758
+ ['This is a very long comment', 'that should be wrapped']
759
+ """
760
+ if not text:
761
+ return []
762
+
763
+ words = text.split()
764
+ lines = []
765
+ current_line = []
766
+ current_length = 0
767
+
768
+ for word in words:
769
+ word_length = len(word) + (1 if current_line else 0)
770
+
771
+ if current_length + word_length > max_length and current_line:
772
+ lines.append(" ".join(current_line))
773
+ current_line = [word]
774
+ current_length = len(word)
775
+ else:
776
+ current_line.append(word)
777
+ current_length += word_length
778
+
779
+ if current_line:
780
+ lines.append(" ".join(current_line))
781
+
782
+ return lines
783
+
784
+ def format_enum_description(self, text: str) -> str:
785
+ """
786
+ Format enum description by splitting bullet points.
787
+
788
+ Enum descriptions from OpenAPI often have the format:
789
+ "* `value1` - Desc1 * `value2` - Desc2"
790
+
791
+ This method splits them into separate lines:
792
+ "* `value1` - Desc1\n* `value2` - Desc2"
793
+
794
+ Args:
795
+ text: Enum description text
796
+
797
+ Returns:
798
+ Formatted description with proper line breaks
799
+ """
800
+ if not text:
801
+ return text
802
+
803
+ # Split by " * `" pattern (preserving the first *)
804
+ import re
805
+ # Replace " * `" with newline + "* `"
806
+ formatted = re.sub(r'\s+\*\s+`', '\n* `', text.strip())
807
+
808
+ return formatted
809
+
810
+ def sanitize_enum_name(self, name: str) -> str:
811
+ """
812
+ Sanitize enum name by converting to PascalCase.
813
+
814
+ Examples:
815
+ "OrderDetail.status" -> "OrderDetailStatus"
816
+ "Currency.currency_type" -> "CurrencyCurrencyType"
817
+ "CurrencyList.currency_type" -> "CurrencyListCurrencyType"
818
+ "User.role" -> "UserRole"
819
+
820
+ Args:
821
+ name: Original enum name (may contain dots, underscores)
822
+
823
+ Returns:
824
+ Sanitized PascalCase name
825
+ """
826
+ # Replace dots with underscores, then split and convert to PascalCase
827
+ parts = name.replace('.', '_').split('_')
828
+ result = []
829
+ for word in parts:
830
+ if not word:
831
+ continue
832
+ # If word is already PascalCase/camelCase, keep it as is
833
+ # Otherwise capitalize first letter only
834
+ if word[0].isupper():
835
+ result.append(word)
836
+ else:
837
+ result.append(word[0].upper() + word[1:] if len(word) > 1 else word.upper())
838
+ return ''.join(result)