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.
- django_cfg/apps/agents/management/commands/create_agent.py +1 -1
- django_cfg/apps/agents/management/commands/orchestrator_status.py +3 -3
- django_cfg/apps/newsletter/serializers.py +40 -3
- django_cfg/apps/newsletter/views/campaigns.py +12 -3
- django_cfg/apps/newsletter/views/emails.py +14 -3
- django_cfg/apps/newsletter/views/subscriptions.py +12 -2
- django_cfg/apps/payments/views/api/currencies.py +49 -6
- django_cfg/apps/payments/views/api/webhooks.py +72 -7
- django_cfg/apps/payments/views/overview/serializers.py +34 -1
- django_cfg/apps/payments/views/overview/views.py +2 -1
- django_cfg/apps/payments/views/serializers/payments.py +6 -6
- django_cfg/apps/urls.py +106 -45
- django_cfg/core/base/config_model.py +2 -2
- django_cfg/core/constants.py +1 -1
- django_cfg/core/generation/integration_generators/__init__.py +1 -1
- django_cfg/core/generation/integration_generators/api.py +73 -49
- django_cfg/core/integration/display/startup.py +30 -22
- django_cfg/core/integration/url_integration.py +15 -16
- django_cfg/management/commands/check_endpoints.py +11 -160
- django_cfg/management/commands/check_settings.py +13 -348
- django_cfg/management/commands/clear_constance.py +13 -201
- django_cfg/management/commands/create_token.py +13 -321
- django_cfg/management/commands/generate_clients.py +23 -0
- django_cfg/management/commands/list_urls.py +13 -306
- django_cfg/management/commands/migrate_all.py +13 -126
- django_cfg/management/commands/migrator.py +13 -396
- django_cfg/management/commands/rundramatiq.py +15 -247
- django_cfg/management/commands/rundramatiq_simulator.py +12 -429
- django_cfg/management/commands/runserver_ngrok.py +15 -160
- django_cfg/management/commands/script.py +12 -488
- django_cfg/management/commands/show_config.py +12 -215
- django_cfg/management/commands/show_urls.py +12 -342
- django_cfg/management/commands/superuser.py +15 -295
- django_cfg/management/commands/task_clear.py +14 -217
- django_cfg/management/commands/task_status.py +13 -248
- django_cfg/management/commands/test_email.py +15 -86
- django_cfg/management/commands/test_telegram.py +14 -61
- django_cfg/management/commands/test_twilio.py +15 -105
- django_cfg/management/commands/tree.py +13 -383
- django_cfg/management/commands/validate_openapi.py +10 -0
- django_cfg/middleware/README.md +1 -1
- django_cfg/middleware/user_activity.py +3 -3
- django_cfg/models/__init__.py +2 -2
- django_cfg/models/api/drf/spectacular.py +6 -6
- django_cfg/models/django/__init__.py +2 -2
- django_cfg/models/django/openapi.py +162 -0
- django_cfg/modules/django_admin/management/commands/check_endpoints.py +169 -0
- django_cfg/modules/django_admin/management/commands/check_settings.py +355 -0
- django_cfg/modules/django_admin/management/commands/clear_constance.py +208 -0
- django_cfg/modules/django_admin/management/commands/create_token.py +328 -0
- django_cfg/modules/django_admin/management/commands/list_urls.py +313 -0
- django_cfg/modules/django_admin/management/commands/migrate_all.py +133 -0
- django_cfg/modules/django_admin/management/commands/migrator.py +403 -0
- django_cfg/modules/django_admin/management/commands/script.py +496 -0
- django_cfg/modules/django_admin/management/commands/show_config.py +225 -0
- django_cfg/modules/django_admin/management/commands/show_urls.py +361 -0
- django_cfg/modules/django_admin/management/commands/superuser.py +302 -0
- django_cfg/modules/django_admin/management/commands/tree.py +390 -0
- django_cfg/modules/django_client/__init__.py +20 -0
- django_cfg/modules/django_client/apps.py +35 -0
- django_cfg/modules/django_client/core/__init__.py +56 -0
- django_cfg/modules/django_client/core/archive/__init__.py +11 -0
- django_cfg/modules/django_client/core/archive/manager.py +134 -0
- django_cfg/modules/django_client/core/cli/__init__.py +12 -0
- django_cfg/modules/django_client/core/cli/main.py +235 -0
- django_cfg/modules/django_client/core/config/__init__.py +18 -0
- django_cfg/modules/django_client/core/config/config.py +208 -0
- django_cfg/modules/django_client/core/config/group.py +101 -0
- django_cfg/modules/django_client/core/config/service.py +209 -0
- django_cfg/modules/django_client/core/generator/__init__.py +115 -0
- django_cfg/modules/django_client/core/generator/base.py +838 -0
- django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
- django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
- django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
- django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
- django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
- django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
- django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
- django_cfg/modules/django_client/core/generator/python/templates/__init__.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/api_wrapper.py.jinja +153 -0
- django_cfg/modules/django_client/core/generator/python/templates/app_init.py.jinja +6 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/app_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/flat_client.py.jinja +38 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/main_client.py.jinja +68 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/main_client_file.py.jinja +14 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
- django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
- django_cfg/modules/django_client/core/generator/python/templates/client_file.py.jinja +13 -0
- django_cfg/modules/django_client/core/generator/python/templates/main_init.py.jinja +52 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/app_models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/enum_class.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/enums.py.jinja +8 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/models.py.jinja +17 -0
- django_cfg/modules/django_client/core/generator/python/templates/models/schema_class.py.jinja +21 -0
- django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/logger.py.jinja +255 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
- django_cfg/modules/django_client/core/generator/python/templates/utils/schema.py.jinja +12 -0
- django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
- django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
- django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
- django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
- django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
- django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +536 -0
- django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
- django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
- django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/app_index.ts.jinja +2 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/app_client.ts.jinja +18 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/client.ts.jinja +403 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/flat_client.ts.jinja +109 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/main_client_file.ts.jinja +10 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/operation.ts.jinja +61 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client/sub_client.ts.jinja +15 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/client_file.ts.jinja +9 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/index.ts.jinja +5 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/main_index.ts.jinja +268 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/app_models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/enums.ts.jinja +4 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/models/models.ts.jinja +8 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/errors.ts.jinja +116 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/http.ts.jinja +98 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/logger.ts.jinja +259 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/schema.ts.jinja +7 -0
- django_cfg/modules/django_client/core/generator/typescript/templates/utils/storage.ts.jinja +158 -0
- django_cfg/modules/django_client/core/groups/__init__.py +13 -0
- django_cfg/modules/django_client/core/groups/detector.py +178 -0
- django_cfg/modules/django_client/core/groups/manager.py +314 -0
- django_cfg/modules/django_client/core/ir/__init__.py +57 -0
- django_cfg/modules/django_client/core/ir/context.py +387 -0
- django_cfg/modules/django_client/core/ir/operation.py +518 -0
- django_cfg/modules/django_client/core/ir/schema.py +353 -0
- django_cfg/modules/django_client/core/parser/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/base.py +648 -0
- django_cfg/modules/django_client/core/parser/models/__init__.py +74 -0
- django_cfg/modules/django_client/core/parser/models/base.py +212 -0
- django_cfg/modules/django_client/core/parser/models/components.py +160 -0
- django_cfg/modules/django_client/core/parser/models/openapi.py +203 -0
- django_cfg/modules/django_client/core/parser/models/operation.py +207 -0
- django_cfg/modules/django_client/core/parser/models/schema.py +266 -0
- django_cfg/modules/django_client/core/parser/openapi30.py +56 -0
- django_cfg/modules/django_client/core/parser/openapi31.py +64 -0
- django_cfg/modules/django_client/core/validation/__init__.py +22 -0
- django_cfg/modules/django_client/core/validation/checker.py +134 -0
- django_cfg/modules/django_client/core/validation/fixer.py +216 -0
- django_cfg/modules/django_client/core/validation/reporter.py +480 -0
- django_cfg/modules/django_client/core/validation/rules/__init__.py +11 -0
- django_cfg/modules/django_client/core/validation/rules/base.py +96 -0
- django_cfg/modules/django_client/core/validation/rules/type_hints.py +288 -0
- django_cfg/modules/django_client/core/validation/safety.py +266 -0
- django_cfg/modules/django_client/management/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/__init__.py +3 -0
- django_cfg/modules/django_client/management/commands/generate_client.py +427 -0
- django_cfg/modules/django_client/management/commands/validate_openapi.py +343 -0
- django_cfg/modules/django_client/pytest.ini +30 -0
- django_cfg/modules/django_client/spectacular/__init__.py +10 -0
- django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
- django_cfg/modules/django_client/spectacular/enum_naming.py +192 -0
- django_cfg/modules/django_client/urls.py +72 -0
- django_cfg/{dashboard → modules/django_dashboard}/DEBUG_README.md +2 -2
- django_cfg/{dashboard → modules/django_dashboard}/REFACTORING_SUMMARY.md +1 -1
- django_cfg/modules/django_dashboard/management/__init__.py +0 -0
- django_cfg/modules/django_dashboard/management/commands/__init__.py +0 -0
- django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
- django_cfg/modules/django_dashboard/sections/documentation.py +391 -0
- django_cfg/modules/django_email/management/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/__init__.py +0 -0
- django_cfg/modules/django_email/management/commands/test_email.py +93 -0
- django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
- django_cfg/modules/django_logging/django_logger.py +6 -6
- django_cfg/modules/django_ngrok/management/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/__init__.py +0 -0
- django_cfg/modules/django_ngrok/management/commands/runserver_ngrok.py +167 -0
- django_cfg/modules/django_tasks/management/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/__init__.py +0 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq.py +254 -0
- django_cfg/modules/django_tasks/management/commands/rundramatiq_simulator.py +437 -0
- django_cfg/modules/django_tasks/management/commands/task_clear.py +226 -0
- django_cfg/modules/django_tasks/management/commands/task_status.py +257 -0
- django_cfg/modules/django_telegram/management/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/__init__.py +0 -0
- django_cfg/modules/django_telegram/management/commands/test_telegram.py +68 -0
- django_cfg/modules/django_twilio/management/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/__init__.py +0 -0
- django_cfg/modules/django_twilio/management/commands/test_twilio.py +112 -0
- django_cfg/modules/django_unfold/callbacks/main.py +21 -10
- django_cfg/modules/django_unfold/callbacks/revolution.py +41 -36
- django_cfg/pyproject.toml +2 -6
- django_cfg/registry/third_party.py +5 -7
- django_cfg/routing/callbacks.py +1 -1
- django_cfg/static/admin/css/prose-unfold.css +666 -0
- django_cfg/templates/admin/index.html +8 -0
- django_cfg/templates/admin/index_new.html +13 -0
- django_cfg/templates/admin/layouts/dashboard_with_tabs.html +15 -3
- django_cfg/templates/admin/sections/documentation_section.html +172 -0
- django_cfg/templates/admin/snippets/tabs/documentation_tab.html +231 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/METADATA +2 -2
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/RECORD +224 -74
- django_cfg/management/commands/generate.py +0 -107
- /django_cfg/models/django/{revolution.py → revolution_legacy.py} +0 -0
- /django_cfg/{dashboard → modules/django_admin}/management/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_admin}/management/commands/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
- /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/WHEEL +0 -0
- {django_cfg-1.4.10.dist-info → django_cfg-1.4.13.dist-info}/entry_points.txt +0 -0
- {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']
|