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,255 @@
1
+ """
2
+ API Logger with Rich
3
+ Beautiful console logging for API requests and responses
4
+
5
+ Installation:
6
+ pip install rich
7
+ """
8
+
9
+ from __future__ import annotations
10
+
11
+ import time
12
+ from dataclasses import dataclass, field
13
+ from typing import Any, Dict, Optional
14
+
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.table import Table
18
+ from rich.text import Text
19
+
20
+
21
+ @dataclass
22
+ class RequestLog:
23
+ """Request log data."""
24
+
25
+ method: str
26
+ url: str
27
+ headers: Optional[Dict[str, str]] = None
28
+ body: Optional[Any] = None
29
+ timestamp: float = field(default_factory=time.time)
30
+
31
+
32
+ @dataclass
33
+ class ResponseLog:
34
+ """Response log data."""
35
+
36
+ status: int
37
+ status_text: str
38
+ data: Optional[Any] = None
39
+ duration: float = 0.0
40
+ timestamp: float = field(default_factory=time.time)
41
+
42
+
43
+ @dataclass
44
+ class ErrorLog:
45
+ """Error log data."""
46
+
47
+ message: str
48
+ status_code: Optional[int] = None
49
+ field_errors: Optional[Dict[str, list[str]]] = None
50
+ duration: float = 0.0
51
+ timestamp: float = field(default_factory=time.time)
52
+
53
+
54
+ @dataclass
55
+ class LoggerConfig:
56
+ """Logger configuration."""
57
+
58
+ enabled: bool = True
59
+ log_requests: bool = True
60
+ log_responses: bool = True
61
+ log_errors: bool = True
62
+ log_bodies: bool = True
63
+ log_headers: bool = False
64
+ console: Optional[Console] = None
65
+
66
+
67
+ # Sensitive header names to filter out
68
+ SENSITIVE_HEADERS = [
69
+ "authorization",
70
+ "cookie",
71
+ "set-cookie",
72
+ "x-api-key",
73
+ "x-csrf-token",
74
+ ]
75
+
76
+
77
+ class APILogger:
78
+ """API Logger class."""
79
+
80
+ def __init__(self, config: Optional[LoggerConfig] = None):
81
+ """Initialize logger."""
82
+ self.config = config or LoggerConfig()
83
+ self.console = self.config.console or Console()
84
+
85
+ def enable(self) -> None:
86
+ """Enable logging."""
87
+ self.config.enabled = True
88
+
89
+ def disable(self) -> None:
90
+ """Disable logging."""
91
+ self.config.enabled = False
92
+
93
+ def set_config(self, **kwargs: Any) -> None:
94
+ """Update configuration."""
95
+ for key, value in kwargs.items():
96
+ if hasattr(self.config, key):
97
+ setattr(self.config, key, value)
98
+
99
+ def _filter_headers(self, headers: Optional[Dict[str, str]]) -> Dict[str, str]:
100
+ """Filter sensitive headers."""
101
+ if not headers:
102
+ return {}
103
+
104
+ filtered = {}
105
+ for key, value in headers.items():
106
+ if key.lower() in SENSITIVE_HEADERS:
107
+ filtered[key] = "***"
108
+ else:
109
+ filtered[key] = value
110
+
111
+ return filtered
112
+
113
+ def log_request(self, request: RequestLog) -> None:
114
+ """Log request."""
115
+ if not self.config.enabled or not self.config.log_requests:
116
+ return
117
+
118
+ # Create request info
119
+ text = Text()
120
+ text.append("→ ", style="bold blue")
121
+ text.append(request.method, style="bold yellow")
122
+ text.append(" ", style="")
123
+ text.append(request.url, style="cyan")
124
+
125
+ self.console.print(text)
126
+
127
+ if self.config.log_headers and request.headers:
128
+ headers = self._filter_headers(request.headers)
129
+ self.console.print(" Headers:", style="dim")
130
+ for key, value in headers.items():
131
+ self.console.print(f" {key}: {value}", style="dim")
132
+
133
+ if self.config.log_bodies and request.body:
134
+ self.console.print(" Body:", style="dim")
135
+ self.console.print(request.body, style="dim")
136
+
137
+ def log_response(self, request: RequestLog, response: ResponseLog) -> None:
138
+ """Log response."""
139
+ if not self.config.enabled or not self.config.log_responses:
140
+ return
141
+
142
+ # Determine color based on status
143
+ if response.status >= 500:
144
+ status_style = "bold red"
145
+ elif response.status >= 400:
146
+ status_style = "bold yellow"
147
+ elif response.status >= 300:
148
+ status_style = "bold cyan"
149
+ else:
150
+ status_style = "bold green"
151
+
152
+ # Create response info
153
+ text = Text()
154
+ text.append("← ", style="bold green")
155
+ text.append(request.method, style="bold yellow")
156
+ text.append(" ", style="")
157
+ text.append(request.url, style="cyan")
158
+ text.append(" ", style="")
159
+ text.append(str(response.status), style=status_style)
160
+ text.append(" ", style="")
161
+ text.append(response.status_text, style=status_style)
162
+ text.append(f" ({response.duration:.0f}ms)", style="dim")
163
+
164
+ self.console.print(text)
165
+
166
+ if self.config.log_bodies and response.data:
167
+ self.console.print(" Response:", style="dim")
168
+ self.console.print(response.data, style="dim")
169
+
170
+ def log_error(self, request: RequestLog, error: ErrorLog) -> None:
171
+ """Log error."""
172
+ if not self.config.enabled or not self.config.log_errors:
173
+ return
174
+
175
+ # Create error header
176
+ text = Text()
177
+ text.append("✗ ", style="bold red")
178
+ text.append(request.method, style="bold yellow")
179
+ text.append(" ", style="")
180
+ text.append(request.url, style="cyan")
181
+ text.append(" ", style="")
182
+ text.append(
183
+ str(error.status_code) if error.status_code else "Network",
184
+ style="bold red",
185
+ )
186
+ text.append(" Error", style="bold red")
187
+ text.append(f" ({error.duration:.0f}ms)", style="dim")
188
+
189
+ self.console.print(text)
190
+ self.console.print(f" Message: {error.message}", style="red")
191
+
192
+ if error.field_errors:
193
+ self.console.print(" Field Errors:", style="red")
194
+ for field, errors in error.field_errors.items():
195
+ for err in errors:
196
+ self.console.print(f" • {field}: {err}", style="red dim")
197
+
198
+ def info(self, message: str, **kwargs: Any) -> None:
199
+ """Log info message."""
200
+ if not self.config.enabled:
201
+ return
202
+ self.console.print(f"ℹ {message}", style="blue", **kwargs)
203
+
204
+ def warn(self, message: str, **kwargs: Any) -> None:
205
+ """Log warning message."""
206
+ if not self.config.enabled:
207
+ return
208
+ self.console.print(f"⚠ {message}", style="yellow", **kwargs)
209
+
210
+ def error(self, message: str, **kwargs: Any) -> None:
211
+ """Log error message."""
212
+ if not self.config.enabled:
213
+ return
214
+ self.console.print(f"✗ {message}", style="red", **kwargs)
215
+
216
+ def success(self, message: str, **kwargs: Any) -> None:
217
+ """Log success message."""
218
+ if not self.config.enabled:
219
+ return
220
+ self.console.print(f"✓ {message}", style="green", **kwargs)
221
+
222
+ def debug(self, message: str, **kwargs: Any) -> None:
223
+ """Log debug message."""
224
+ if not self.config.enabled:
225
+ return
226
+ self.console.print(f"🔍 {message}", style="dim", **kwargs)
227
+
228
+ def panel(self, content: Any, title: str, style: str = "blue") -> None:
229
+ """Log content in a panel."""
230
+ if not self.config.enabled:
231
+ return
232
+ self.console.print(Panel(content, title=title, border_style=style))
233
+
234
+ def table(
235
+ self,
236
+ headers: list[str],
237
+ rows: list[list[Any]],
238
+ title: Optional[str] = None,
239
+ ) -> None:
240
+ """Log data in a table."""
241
+ if not self.config.enabled:
242
+ return
243
+
244
+ table = Table(title=title)
245
+ for header in headers:
246
+ table.add_column(header, style="cyan")
247
+
248
+ for row in rows:
249
+ table.add_row(*[str(cell) for cell in row])
250
+
251
+ self.console.print(table)
252
+
253
+
254
+ # Default logger instance
255
+ default_logger = APILogger()
@@ -0,0 +1,271 @@
1
+ """
2
+ Retry Configuration and Utilities
3
+
4
+ Provides automatic retry logic for failed HTTP requests using tenacity.
5
+ Retries only on network errors and server errors (5xx), not client errors (4xx).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from dataclasses import dataclass, field
11
+ from typing import Callable, Any
12
+ import httpx
13
+ from tenacity import (
14
+ retry,
15
+ stop_after_attempt,
16
+ wait_exponential,
17
+ retry_if_exception,
18
+ RetryCallState,
19
+ before_sleep_log,
20
+ )
21
+ import logging
22
+
23
+
24
+ @dataclass
25
+ class RetryConfig:
26
+ """
27
+ Retry configuration options.
28
+
29
+ Uses exponential backoff with jitter by default to avoid thundering herd.
30
+ """
31
+
32
+ max_attempts: int = 3
33
+ """Maximum number of retry attempts (default: 3)"""
34
+
35
+ min_wait: float = 1.0
36
+ """Minimum wait time between retries in seconds (default: 1.0)"""
37
+
38
+ max_wait: float = 60.0
39
+ """Maximum wait time between retries in seconds (default: 60.0)"""
40
+
41
+ multiplier: float = 2.0
42
+ """Exponential backoff multiplier (default: 2.0)"""
43
+
44
+ on_retry: Callable[[RetryCallState], None] | None = None
45
+ """Callback called on each retry attempt"""
46
+
47
+ logger: logging.Logger | None = None
48
+ """Logger for retry attempts (default: None)"""
49
+
50
+
51
+ DEFAULT_RETRY_CONFIG = RetryConfig()
52
+ """Default retry configuration"""
53
+
54
+
55
+ def should_retry(exception: BaseException) -> bool:
56
+ """
57
+ Determine if an error should trigger a retry.
58
+
59
+ Retries on:
60
+ - Network errors (connection refused, timeout, etc.)
61
+ - Server errors (5xx status codes)
62
+ - Rate limiting (429 status code)
63
+
64
+ Does NOT retry on:
65
+ - Client errors (4xx except 429)
66
+ - Authentication errors (401, 403)
67
+ - Not found (404)
68
+
69
+ Args:
70
+ exception: The exception to check
71
+
72
+ Returns:
73
+ True if should retry, False otherwise
74
+ """
75
+ # Always retry network errors
76
+ if isinstance(exception, (
77
+ httpx.NetworkError,
78
+ httpx.TimeoutException,
79
+ httpx.ConnectError,
80
+ httpx.ReadError,
81
+ httpx.WriteError,
82
+ httpx.PoolTimeout,
83
+ )):
84
+ return True
85
+
86
+ # For HTTP errors, check status code
87
+ if isinstance(exception, httpx.HTTPStatusError):
88
+ status = exception.response.status_code
89
+
90
+ # Retry on 5xx server errors
91
+ if 500 <= status < 600:
92
+ return True
93
+
94
+ # Retry on 429 (rate limit)
95
+ if status == 429:
96
+ return True
97
+
98
+ # Do NOT retry on 4xx client errors
99
+ return False
100
+
101
+ # Don't retry on unknown errors
102
+ return False
103
+
104
+
105
+ def create_retry_decorator(config: RetryConfig | None = None):
106
+ """
107
+ Create a retry decorator with the given configuration.
108
+
109
+ Args:
110
+ config: Retry configuration (uses defaults if None)
111
+
112
+ Returns:
113
+ Tenacity retry decorator
114
+
115
+ Example:
116
+ >>> retry_decorator = create_retry_decorator(RetryConfig(max_attempts=5))
117
+ >>> @retry_decorator
118
+ ... async def fetch_data():
119
+ ... async with httpx.AsyncClient() as client:
120
+ ... response = await client.get('https://api.example.com/users')
121
+ ... response.raise_for_status()
122
+ ... return response.json()
123
+ """
124
+ cfg = config or DEFAULT_RETRY_CONFIG
125
+
126
+ # Build retry decorator
127
+ retry_args = {
128
+ 'stop': stop_after_attempt(cfg.max_attempts),
129
+ 'wait': wait_exponential(
130
+ multiplier=cfg.multiplier,
131
+ min=cfg.min_wait,
132
+ max=cfg.max_wait,
133
+ ),
134
+ 'retry': retry_if_exception(should_retry),
135
+ 'reraise': True,
136
+ }
137
+
138
+ # Add logger if provided
139
+ if cfg.logger:
140
+ retry_args['before_sleep'] = before_sleep_log(cfg.logger, logging.WARNING)
141
+
142
+ # Add custom callback if provided
143
+ if cfg.on_retry:
144
+ original_before_sleep = retry_args.get('before_sleep')
145
+
146
+ def combined_before_sleep(retry_state: RetryCallState):
147
+ if original_before_sleep:
148
+ original_before_sleep(retry_state)
149
+ if cfg.on_retry:
150
+ cfg.on_retry(retry_state)
151
+
152
+ retry_args['before_sleep'] = combined_before_sleep
153
+
154
+ return retry(**retry_args)
155
+
156
+
157
+ async def with_retry(
158
+ fn: Callable[..., Any],
159
+ config: RetryConfig | None = None,
160
+ *args,
161
+ **kwargs
162
+ ) -> Any:
163
+ """
164
+ Execute an async function with retry logic.
165
+
166
+ Args:
167
+ fn: Async function to retry
168
+ config: Retry configuration (uses defaults if None)
169
+ *args: Positional arguments for fn
170
+ **kwargs: Keyword arguments for fn
171
+
172
+ Returns:
173
+ Result of the function
174
+
175
+ Example:
176
+ >>> async def fetch_users():
177
+ ... async with httpx.AsyncClient() as client:
178
+ ... response = await client.get('https://api.example.com/users')
179
+ ... response.raise_for_status()
180
+ ... return response.json()
181
+ >>>
182
+ >>> result = await with_retry(fetch_users, RetryConfig(max_attempts=5))
183
+ """
184
+ retry_decorator = create_retry_decorator(config)
185
+ retryable_fn = retry_decorator(fn)
186
+ return await retryable_fn(*args, **kwargs)
187
+
188
+
189
+ class RetryAsyncClient:
190
+ """
191
+ HTTP client wrapper that adds automatic retry logic.
192
+
193
+ Wraps httpx.AsyncClient and applies retry logic to all HTTP methods.
194
+ Transparently retries on network errors, 5xx status codes, and 429 rate limits.
195
+
196
+ Example:
197
+ >>> async with RetryAsyncClient('https://api.example.com', retry_config=RetryConfig(max_attempts=5)) as client:
198
+ ... response = await client.get('/users')
199
+ ... response.raise_for_status()
200
+ """
201
+
202
+ def __init__(
203
+ self,
204
+ base_url: str | None = None,
205
+ retry_config: RetryConfig | None = None,
206
+ **kwargs: Any
207
+ ):
208
+ """
209
+ Initialize retry-enabled HTTP client.
210
+
211
+ Args:
212
+ base_url: Base URL for all requests
213
+ retry_config: Retry configuration (None to disable retry)
214
+ **kwargs: Additional httpx.AsyncClient kwargs
215
+ """
216
+ self._client = httpx.AsyncClient(base_url=base_url, **kwargs)
217
+ self.retry_config = retry_config
218
+ self._retry_decorator = create_retry_decorator(retry_config) if retry_config else None
219
+
220
+ async def __aenter__(self) -> 'RetryAsyncClient':
221
+ await self._client.__aenter__()
222
+ return self
223
+
224
+ async def __aexit__(self, *args: Any) -> None:
225
+ await self._client.__aexit__(*args)
226
+
227
+ async def aclose(self) -> None:
228
+ """Close the HTTP client."""
229
+ await self._client.aclose()
230
+
231
+ def _wrap_with_retry(self, method: str):
232
+ """Wrap HTTP method with retry logic."""
233
+ original_method = getattr(self._client, method)
234
+
235
+ if self._retry_decorator:
236
+ async def wrapped(*args, **kwargs):
237
+ @self._retry_decorator
238
+ async def _do_request():
239
+ return await original_method(*args, **kwargs)
240
+ return await _do_request()
241
+ return wrapped
242
+ else:
243
+ return original_method
244
+
245
+ async def get(self, *args, **kwargs) -> httpx.Response:
246
+ """GET request with retry."""
247
+ return await self._wrap_with_retry('get')(*args, **kwargs)
248
+
249
+ async def post(self, *args, **kwargs) -> httpx.Response:
250
+ """POST request with retry."""
251
+ return await self._wrap_with_retry('post')(*args, **kwargs)
252
+
253
+ async def put(self, *args, **kwargs) -> httpx.Response:
254
+ """PUT request with retry."""
255
+ return await self._wrap_with_retry('put')(*args, **kwargs)
256
+
257
+ async def patch(self, *args, **kwargs) -> httpx.Response:
258
+ """PATCH request with retry."""
259
+ return await self._wrap_with_retry('patch')(*args, **kwargs)
260
+
261
+ async def delete(self, *args, **kwargs) -> httpx.Response:
262
+ """DELETE request with retry."""
263
+ return await self._wrap_with_retry('delete')(*args, **kwargs)
264
+
265
+ async def head(self, *args, **kwargs) -> httpx.Response:
266
+ """HEAD request with retry."""
267
+ return await self._wrap_with_retry('head')(*args, **kwargs)
268
+
269
+ async def options(self, *args, **kwargs) -> httpx.Response:
270
+ """OPTIONS request with retry."""
271
+ return await self._wrap_with_retry('options')(*args, **kwargs)
@@ -0,0 +1,12 @@
1
+ """
2
+ OpenAPI Schema
3
+
4
+ This file contains the complete OpenAPI specification for this API.
5
+ It can be used for documentation, validation, or code generation.
6
+ """
7
+
8
+ from typing import Any, Dict
9
+
10
+ OPENAPI_SCHEMA: Dict[str, Any] = {{ schema_dict }}
11
+
12
+ __all__ = ["OPENAPI_SCHEMA"]
@@ -0,0 +1,14 @@
1
+ """
2
+ TypeScript Generator - Generates TypeScript client (Fetch API).
3
+
4
+ This generator creates a complete TypeScript API client from IR:
5
+ - TypeScript interfaces (Request/Response/Patch splits)
6
+ - Enum types from x-enum-varnames
7
+ - Fetch API for HTTP
8
+ - Django CSRF/session handling
9
+ - Type-safe
10
+ """
11
+
12
+ from .generator import TypeScriptGenerator
13
+
14
+ __all__ = ['TypeScriptGenerator']