django-cfg 1.4.11__py3-none-any.whl → 1.4.14__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 (109) hide show
  1. django_cfg/apps/urls.py +120 -108
  2. django_cfg/core/generation/integration_generators/api.py +2 -1
  3. django_cfg/core/integration/url_integration.py +5 -10
  4. django_cfg/models/django/openapi.py +15 -128
  5. django_cfg/modules/django_client/core/archive/manager.py +2 -2
  6. django_cfg/modules/django_client/core/config/config.py +20 -0
  7. django_cfg/modules/django_client/core/config/service.py +1 -1
  8. django_cfg/modules/django_client/core/generator/__init__.py +4 -4
  9. django_cfg/modules/django_client/core/generator/base.py +71 -0
  10. django_cfg/modules/django_client/core/generator/python/__init__.py +16 -0
  11. django_cfg/modules/django_client/core/generator/python/async_client_gen.py +174 -0
  12. django_cfg/modules/django_client/core/generator/python/files_generator.py +180 -0
  13. django_cfg/modules/django_client/core/generator/python/generator.py +182 -0
  14. django_cfg/modules/django_client/core/generator/python/models_generator.py +318 -0
  15. django_cfg/modules/django_client/core/generator/python/operations_generator.py +278 -0
  16. django_cfg/modules/django_client/core/generator/python/sync_client_gen.py +102 -0
  17. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/api_wrapper.py.jinja +25 -2
  18. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client.py.jinja +24 -6
  19. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/main_client_file.py.jinja +1 -0
  20. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/operation_method.py.jinja +3 -1
  21. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/sub_client.py.jinja +8 -1
  22. django_cfg/modules/django_client/core/generator/python/templates/client/sync_main_client.py.jinja +50 -0
  23. django_cfg/modules/django_client/core/generator/python/templates/client/sync_operation_method.py.jinja +9 -0
  24. django_cfg/modules/django_client/core/generator/python/templates/client/sync_sub_client.py.jinja +18 -0
  25. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/main_init.py.jinja +2 -0
  26. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enum_class.py.jinja +3 -1
  27. django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/schema_class.py.jinja +3 -1
  28. django_cfg/modules/django_client/core/generator/python/templates/pyproject.toml.jinja +55 -0
  29. django_cfg/modules/django_client/core/generator/python/templates/utils/retry.py.jinja +271 -0
  30. django_cfg/modules/django_client/core/generator/typescript/__init__.py +14 -0
  31. django_cfg/modules/django_client/core/generator/typescript/client_generator.py +165 -0
  32. django_cfg/modules/django_client/core/generator/typescript/fetchers_generator.py +428 -0
  33. django_cfg/modules/django_client/core/generator/typescript/files_generator.py +207 -0
  34. django_cfg/modules/django_client/core/generator/typescript/generator.py +432 -0
  35. django_cfg/modules/django_client/core/generator/typescript/hooks_generator.py +539 -0
  36. django_cfg/modules/django_client/core/generator/typescript/models_generator.py +245 -0
  37. django_cfg/modules/django_client/core/generator/typescript/operations_generator.py +298 -0
  38. django_cfg/modules/django_client/core/generator/typescript/schemas_generator.py +329 -0
  39. django_cfg/modules/django_client/core/generator/typescript/templates/api_instance.ts.jinja +131 -0
  40. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/app_client.ts.jinja +1 -1
  41. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/client.ts.jinja +77 -1
  42. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/main_client_file.ts.jinja +1 -0
  43. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/sub_client.ts.jinja +3 -3
  44. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/fetchers.ts.jinja +45 -0
  45. django_cfg/modules/django_client/core/generator/typescript/templates/fetchers/index.ts.jinja +30 -0
  46. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/main_index.ts.jinja +73 -11
  47. django_cfg/modules/django_client/core/generator/typescript/templates/package.json.jinja +52 -0
  48. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/index.ts.jinja +21 -0
  49. django_cfg/modules/django_client/core/generator/typescript/templates/schemas/schema.ts.jinja +24 -0
  50. django_cfg/modules/django_client/core/generator/typescript/templates/tsconfig.json.jinja +20 -0
  51. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/errors.ts.jinja +3 -1
  52. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/logger.ts.jinja +9 -1
  53. django_cfg/modules/django_client/core/generator/typescript/templates/utils/retry.ts.jinja +175 -0
  54. django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/storage.ts.jinja +54 -10
  55. django_cfg/modules/django_client/management/commands/generate_client.py +5 -0
  56. django_cfg/modules/django_client/pytest.ini +30 -0
  57. django_cfg/modules/django_client/spectacular/__init__.py +3 -2
  58. django_cfg/modules/django_client/spectacular/async_detection.py +187 -0
  59. django_cfg/{dashboard → modules/django_dashboard}/management/commands/debug_dashboard.py +5 -5
  60. django_cfg/modules/django_logging/LOGGING_GUIDE.md +1 -1
  61. django_cfg/modules/django_unfold/callbacks/main.py +6 -6
  62. django_cfg/modules/django_unfold/dashboard.py +6 -6
  63. django_cfg/pyproject.toml +1 -1
  64. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/METADATA +1 -1
  65. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/RECORD +100 -78
  66. django_cfg/dashboard/DEBUG_README.md +0 -105
  67. django_cfg/dashboard/REFACTORING_SUMMARY.md +0 -237
  68. django_cfg/modules/django_client/core/generator/python.py +0 -751
  69. django_cfg/modules/django_client/core/generator/typescript.py +0 -872
  70. django_cfg/modules/django_drf_theme/CHANGELOG.md +0 -210
  71. django_cfg/modules/django_drf_theme/EXAMPLE.md +0 -465
  72. django_cfg/modules/django_drf_theme/IMPLEMENTATION.md +0 -232
  73. django_cfg/modules/django_drf_theme/README.md +0 -207
  74. django_cfg/modules/django_drf_theme/TAILWIND_CDN_GUIDE.md +0 -274
  75. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/__init__.py.jinja +0 -0
  76. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/app_init.py.jinja +0 -0
  77. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/app_client.py.jinja +0 -0
  78. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client/flat_client.py.jinja +0 -0
  79. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/client_file.py.jinja +0 -0
  80. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/app_models.py.jinja +0 -0
  81. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/enums.py.jinja +0 -0
  82. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/models/models.py.jinja +0 -0
  83. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/logger.py.jinja +0 -0
  84. /django_cfg/modules/django_client/core/generator/{templates/python → python/templates}/utils/schema.py.jinja +0 -0
  85. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/app_index.ts.jinja +0 -0
  86. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/flat_client.ts.jinja +0 -0
  87. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client/operation.ts.jinja +0 -0
  88. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/client_file.ts.jinja +0 -0
  89. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/index.ts.jinja +0 -0
  90. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/app_models.ts.jinja +0 -0
  91. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/enums.ts.jinja +0 -0
  92. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/models/models.ts.jinja +0 -0
  93. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/http.ts.jinja +0 -0
  94. /django_cfg/modules/django_client/core/generator/{templates/typescript → typescript/templates}/utils/schema.ts.jinja +0 -0
  95. /django_cfg/{dashboard → modules/django_dashboard}/__init__.py +0 -0
  96. /django_cfg/{dashboard → modules/django_dashboard}/components.py +0 -0
  97. /django_cfg/{dashboard → modules/django_dashboard}/debug.py +0 -0
  98. /django_cfg/{dashboard → modules/django_dashboard}/management/__init__.py +0 -0
  99. /django_cfg/{dashboard → modules/django_dashboard}/management/commands/__init__.py +0 -0
  100. /django_cfg/{dashboard → modules/django_dashboard}/sections/__init__.py +0 -0
  101. /django_cfg/{dashboard → modules/django_dashboard}/sections/base.py +0 -0
  102. /django_cfg/{dashboard → modules/django_dashboard}/sections/commands.py +0 -0
  103. /django_cfg/{dashboard → modules/django_dashboard}/sections/documentation.py +0 -0
  104. /django_cfg/{dashboard → modules/django_dashboard}/sections/overview.py +0 -0
  105. /django_cfg/{dashboard → modules/django_dashboard}/sections/stats.py +0 -0
  106. /django_cfg/{dashboard → modules/django_dashboard}/sections/system.py +0 -0
  107. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/WHEEL +0 -0
  108. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/entry_points.txt +0 -0
  109. {django_cfg-1.4.11.dist-info → django_cfg-1.4.14.dist-info}/licenses/LICENSE +0 -0
@@ -6,24 +6,42 @@ class API:
6
6
  - Thread-safe JWT token storage
7
7
  - Automatic Authorization header injection
8
8
  - Context manager support for async operations
9
+ - Optional retry and logging configuration
9
10
 
10
11
  Example:
11
12
  >>> api = API('https://api.example.com')
12
13
  >>> api.set_token('jwt-token')
13
14
  >>> async with api:
14
15
  ... users = await api.users.list()
16
+ >>>
17
+ >>> # With retry and logging
18
+ >>> api = API(
19
+ ... 'https://api.example.com',
20
+ ... retry_config=RetryConfig(max_attempts=5),
21
+ ... logger_config=LoggerConfig(enabled=True)
22
+ ... )
15
23
  """
16
24
 
17
- def __init__(self, base_url: str, **kwargs: Any):
25
+ def __init__(
26
+ self,
27
+ base_url: str,
28
+ logger_config: LoggerConfig | None = None,
29
+ retry_config: RetryConfig | None = None,
30
+ **kwargs: Any
31
+ ):
18
32
  """
19
33
  Initialize API client.
20
34
 
21
35
  Args:
22
36
  base_url: Base API URL (e.g., 'https://api.example.com')
37
+ logger_config: Logger configuration (None to disable logging)
38
+ retry_config: Retry configuration (None to disable retry)
23
39
  **kwargs: Additional httpx.AsyncClient kwargs
24
40
  """
25
41
  self.base_url = base_url.rstrip('/')
26
42
  self._kwargs = kwargs
43
+ self._logger_config = logger_config
44
+ self._retry_config = retry_config
27
45
  self._token: str | None = None
28
46
  self._refresh_token: str | None = None
29
47
  self._lock = threading.Lock()
@@ -42,7 +60,12 @@ class API:
42
60
  kwargs['headers'] = headers
43
61
 
44
62
  # Create new APIClient
45
- self._client = APIClient(self.base_url, **kwargs)
63
+ self._client = APIClient(
64
+ self.base_url,
65
+ logger_config=self._logger_config,
66
+ retry_config=self._retry_config,
67
+ **kwargs
68
+ )
46
69
 
47
70
  {% for prop in properties %}
48
71
  @property
@@ -6,12 +6,18 @@ class APIClient:
6
6
  >>> async with APIClient(base_url='https://api.example.com') as client:
7
7
  ... users = await client.users.list()
8
8
  ... post = await client.posts.create(data=new_post)
9
+ >>>
10
+ >>> # With retry configuration
11
+ >>> retry_config = RetryConfig(max_attempts=5, min_wait=2.0)
12
+ >>> async with APIClient(base_url='https://api.example.com', retry_config=retry_config) as client:
13
+ ... users = await client.users.list()
9
14
  """
10
15
 
11
16
  def __init__(
12
17
  self,
13
18
  base_url: str,
14
19
  logger_config: Optional[LoggerConfig] = None,
20
+ retry_config: Optional[RetryConfig] = None,
15
21
  **kwargs: Any,
16
22
  ):
17
23
  """
@@ -20,14 +26,25 @@ class APIClient:
20
26
  Args:
21
27
  base_url: Base API URL (e.g., 'https://api.example.com')
22
28
  logger_config: Logger configuration (None to disable logging)
29
+ retry_config: Retry configuration (None to disable retry)
23
30
  **kwargs: Additional httpx.AsyncClient kwargs
24
31
  """
25
32
  self.base_url = base_url.rstrip('/')
26
- self._client = httpx.AsyncClient(
27
- base_url=self.base_url,
28
- timeout=30.0,
29
- **kwargs,
30
- )
33
+
34
+ # Create HTTP client with or without retry
35
+ if retry_config is not None:
36
+ self._client = RetryAsyncClient(
37
+ base_url=self.base_url,
38
+ retry_config=retry_config,
39
+ timeout=30.0,
40
+ **kwargs,
41
+ )
42
+ else:
43
+ self._client = httpx.AsyncClient(
44
+ base_url=self.base_url,
45
+ timeout=30.0,
46
+ **kwargs,
47
+ )
31
48
 
32
49
  # Initialize logger
33
50
  self.logger: Optional[APILogger] = None
@@ -40,10 +57,11 @@ class APIClient:
40
57
  {% endfor %}
41
58
 
42
59
  async def __aenter__(self) -> 'APIClient':
60
+ await self._client.__aenter__()
43
61
  return self
44
62
 
45
63
  async def __aexit__(self, *args: Any) -> None:
46
- await self._client.aclose()
64
+ await self._client.__aexit__(*args)
47
65
 
48
66
  async def close(self) -> None:
49
67
  """Close HTTP client."""
@@ -8,6 +8,7 @@ import httpx
8
8
  from .{{ tag.slug }} import {{ tag.class_name }}
9
9
  {% endfor %}
10
10
  from .logger import APILogger, LoggerConfig
11
+ from .retry import RetryConfig, RetryAsyncClient
11
12
 
12
13
 
13
14
  {{ client_code }}
@@ -1,6 +1,8 @@
1
1
  async def {{ method_name }}({{ params | join(', ') }}) -> {{ return_type }}:
2
2
  {% if docstring %}
3
- """{{ docstring }}"""
3
+ """
4
+ {{ docstring | indent(4, first=True) }}
5
+ """
4
6
  {% endif %}
5
7
  {% for line in body_lines %}
6
8
  {{ line }}
@@ -1,3 +1,10 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from .models import *
6
+
7
+
1
8
  class {{ class_name }}:
2
9
  """API endpoints for {{ tag }}."""
3
10
 
@@ -6,6 +13,6 @@ class {{ class_name }}:
6
13
  self._client = client
7
14
 
8
15
  {% for operation in operations %}
9
- {{ operation }}
16
+ {{ operation | indent(4, first=True) }}
10
17
 
11
18
  {% endfor %}
@@ -0,0 +1,50 @@
1
+ class SyncAPIClient:
2
+ """
3
+ Synchronous API client for {{ api_title }}.
4
+
5
+ Usage:
6
+ >>> with SyncAPIClient(base_url='https://api.example.com') as client:
7
+ ... users = client.users.list()
8
+ ... post = client.posts.create(data=new_post)
9
+ """
10
+
11
+ def __init__(
12
+ self,
13
+ base_url: str,
14
+ logger_config: Optional[LoggerConfig] = None,
15
+ **kwargs: Any,
16
+ ):
17
+ """
18
+ Initialize sync API client.
19
+
20
+ Args:
21
+ base_url: Base API URL (e.g., 'https://api.example.com')
22
+ logger_config: Logger configuration (None to disable logging)
23
+ **kwargs: Additional httpx.Client kwargs
24
+ """
25
+ self.base_url = base_url.rstrip('/')
26
+ self._client = httpx.Client(
27
+ base_url=self.base_url,
28
+ timeout=30.0,
29
+ **kwargs,
30
+ )
31
+
32
+ # Initialize logger
33
+ self.logger: Optional[APILogger] = None
34
+ if logger_config is not None:
35
+ self.logger = APILogger(logger_config)
36
+
37
+ # Initialize sub-clients
38
+ {% for tag in tags %}
39
+ self.{{ tag.property }} = Sync{{ tag.class_name }}(self._client)
40
+ {% endfor %}
41
+
42
+ def __enter__(self) -> 'SyncAPIClient':
43
+ return self
44
+
45
+ def __exit__(self, *args: Any) -> None:
46
+ self._client.close()
47
+
48
+ def close(self) -> None:
49
+ """Close HTTP client."""
50
+ self._client.close()
@@ -0,0 +1,9 @@
1
+ def {{ method_name }}({{ params | join(', ') }}) -> {{ return_type }}:
2
+ {% if docstring %}
3
+ """
4
+ {{ docstring | indent(4, first=True) }}
5
+ """
6
+ {% endif %}
7
+ {% for line in body_lines %}
8
+ {{ line }}
9
+ {% endfor %}
@@ -0,0 +1,18 @@
1
+ from __future__ import annotations
2
+
3
+ import httpx
4
+
5
+ from .models import *
6
+
7
+
8
+ class Sync{{ class_name }}:
9
+ """Synchronous API endpoints for {{ tag }}."""
10
+
11
+ def __init__(self, client: httpx.Client):
12
+ """Initialize sync sub-client with shared httpx client."""
13
+ self._client = client
14
+
15
+ {% for operation in operations %}
16
+ {{ operation | indent(4, first=True) }}
17
+
18
+ {% endfor %}
@@ -31,6 +31,8 @@ import httpx
31
31
 
32
32
  from .client import APIClient
33
33
  from .schema import OPENAPI_SCHEMA
34
+ from .logger import LoggerConfig
35
+ from .retry import RetryConfig
34
36
  {% for tag in tags %}
35
37
  from .{{ tag.slug }} import {{ tag.class_name }}
36
38
  {% endfor %}
@@ -1,6 +1,8 @@
1
1
  class {{ name }}({{ base_class }}):
2
2
  {% if docstring %}
3
- """{{ docstring }}"""
3
+ """
4
+ {{ docstring | indent(4, first=True) }}
5
+ """
4
6
  {% endif %}
5
7
  {% if members %}
6
8
  {% if docstring %}
@@ -1,6 +1,8 @@
1
1
  class {{ name }}(BaseModel):
2
2
  {% if docstring %}
3
- """{{ docstring }}"""
3
+ """
4
+ {{ docstring | indent(4, first=True) }}
5
+ """
4
6
  {% endif %}
5
7
 
6
8
  model_config = ConfigDict(
@@ -0,0 +1,55 @@
1
+ [tool.poetry]
2
+ name = "{{ package_name }}"
3
+ version = "{{ version }}"
4
+ description = "{{ description }}"
5
+ authors = {{ authors | tojson }}
6
+ license = "{{ license }}"
7
+ readme = "README.md"
8
+ {% if repository_url %}repository = "{{ repository_url }}"
9
+ {% endif %}keywords = {{ keywords | tojson }}
10
+ classifiers = [
11
+ "Development Status :: 5 - Production/Stable",
12
+ "Intended Audience :: Developers",
13
+ "License :: OSI Approved :: MIT License",
14
+ "Programming Language :: Python :: 3",
15
+ "Programming Language :: Python :: 3.12",
16
+ "Typing :: Typed",
17
+ ]
18
+
19
+ [tool.poetry.dependencies]
20
+ python = "{{ python_version }}"
21
+ pydantic = "^2.12"
22
+ httpx = "^0.28"
23
+ tenacity = "^9.1"
24
+ rich = "^14.1.0"
25
+
26
+ [tool.poetry.group.dev.dependencies]
27
+ pytest = "^8.0"
28
+ pytest-asyncio = "^0.24"
29
+ pytest-cov = "^6.0"
30
+ mypy = "^1.18"
31
+ ruff = "^0.13"
32
+
33
+ [build-system]
34
+ requires = ["poetry-core"]
35
+ build-backend = "poetry.core.masonry.api"
36
+
37
+ [tool.mypy]
38
+ python_version = "3.12"
39
+ strict = true
40
+ warn_return_any = true
41
+ warn_unused_configs = true
42
+ disallow_untyped_defs = true
43
+
44
+ [tool.ruff]
45
+ line-length = 100
46
+ target-version = "py312"
47
+
48
+ [tool.ruff.lint]
49
+ select = ["E", "F", "I", "N", "UP", "B"]
50
+ ignore = []
51
+
52
+ [tool.pytest.ini_options]
53
+ asyncio_mode = "auto"
54
+ testpaths = ["tests"]
55
+ addopts = "--cov={{ package_name | replace('-', '_') }} --cov-report=html --cov-report=term"
@@ -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,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']