aury-boot 0.0.2__py3-none-any.whl → 0.0.3__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 (138) hide show
  1. aury/boot/__init__.py +66 -0
  2. aury/boot/_version.py +2 -2
  3. aury/boot/application/__init__.py +120 -0
  4. aury/boot/application/app/__init__.py +39 -0
  5. aury/boot/application/app/base.py +511 -0
  6. aury/boot/application/app/components.py +434 -0
  7. aury/boot/application/app/middlewares.py +101 -0
  8. aury/boot/application/config/__init__.py +44 -0
  9. aury/boot/application/config/settings.py +663 -0
  10. aury/boot/application/constants/__init__.py +19 -0
  11. aury/boot/application/constants/components.py +50 -0
  12. aury/boot/application/constants/scheduler.py +28 -0
  13. aury/boot/application/constants/service.py +29 -0
  14. aury/boot/application/errors/__init__.py +55 -0
  15. aury/boot/application/errors/chain.py +80 -0
  16. aury/boot/application/errors/codes.py +67 -0
  17. aury/boot/application/errors/exceptions.py +238 -0
  18. aury/boot/application/errors/handlers.py +320 -0
  19. aury/boot/application/errors/response.py +120 -0
  20. aury/boot/application/interfaces/__init__.py +76 -0
  21. aury/boot/application/interfaces/egress.py +224 -0
  22. aury/boot/application/interfaces/ingress.py +98 -0
  23. aury/boot/application/middleware/__init__.py +22 -0
  24. aury/boot/application/middleware/logging.py +451 -0
  25. aury/boot/application/migrations/__init__.py +13 -0
  26. aury/boot/application/migrations/manager.py +685 -0
  27. aury/boot/application/migrations/setup.py +237 -0
  28. aury/boot/application/rpc/__init__.py +63 -0
  29. aury/boot/application/rpc/base.py +108 -0
  30. aury/boot/application/rpc/client.py +294 -0
  31. aury/boot/application/rpc/discovery.py +218 -0
  32. aury/boot/application/scheduler/__init__.py +13 -0
  33. aury/boot/application/scheduler/runner.py +123 -0
  34. aury/boot/application/server/__init__.py +296 -0
  35. aury/boot/commands/__init__.py +30 -0
  36. aury/boot/commands/add.py +76 -0
  37. aury/boot/commands/app.py +105 -0
  38. aury/boot/commands/config.py +177 -0
  39. aury/boot/commands/docker.py +367 -0
  40. aury/boot/commands/docs.py +284 -0
  41. aury/boot/commands/generate.py +1277 -0
  42. aury/boot/commands/init.py +890 -0
  43. aury/boot/commands/migrate/__init__.py +37 -0
  44. aury/boot/commands/migrate/app.py +54 -0
  45. aury/boot/commands/migrate/commands.py +303 -0
  46. aury/boot/commands/scheduler.py +124 -0
  47. aury/boot/commands/server/__init__.py +21 -0
  48. aury/boot/commands/server/app.py +541 -0
  49. aury/boot/commands/templates/generate/api.py.tpl +105 -0
  50. aury/boot/commands/templates/generate/model.py.tpl +17 -0
  51. aury/boot/commands/templates/generate/repository.py.tpl +19 -0
  52. aury/boot/commands/templates/generate/schema.py.tpl +29 -0
  53. aury/boot/commands/templates/generate/service.py.tpl +48 -0
  54. aury/boot/commands/templates/project/CLI.md.tpl +92 -0
  55. aury/boot/commands/templates/project/DEVELOPMENT.md.tpl +1397 -0
  56. aury/boot/commands/templates/project/README.md.tpl +111 -0
  57. aury/boot/commands/templates/project/admin_console_init.py.tpl +50 -0
  58. aury/boot/commands/templates/project/config.py.tpl +30 -0
  59. aury/boot/commands/templates/project/conftest.py.tpl +26 -0
  60. aury/boot/commands/templates/project/env.example.tpl +213 -0
  61. aury/boot/commands/templates/project/gitignore.tpl +128 -0
  62. aury/boot/commands/templates/project/main.py.tpl +41 -0
  63. aury/boot/commands/templates/project/modules/api.py.tpl +19 -0
  64. aury/boot/commands/templates/project/modules/exceptions.py.tpl +84 -0
  65. aury/boot/commands/templates/project/modules/schedules.py.tpl +18 -0
  66. aury/boot/commands/templates/project/modules/tasks.py.tpl +20 -0
  67. aury/boot/commands/worker.py +143 -0
  68. aury/boot/common/__init__.py +35 -0
  69. aury/boot/common/exceptions/__init__.py +114 -0
  70. aury/boot/common/i18n/__init__.py +16 -0
  71. aury/boot/common/i18n/translator.py +272 -0
  72. aury/boot/common/logging/__init__.py +716 -0
  73. aury/boot/contrib/__init__.py +10 -0
  74. aury/boot/contrib/admin_console/__init__.py +18 -0
  75. aury/boot/contrib/admin_console/auth.py +137 -0
  76. aury/boot/contrib/admin_console/discovery.py +69 -0
  77. aury/boot/contrib/admin_console/install.py +172 -0
  78. aury/boot/contrib/admin_console/utils.py +44 -0
  79. aury/boot/domain/__init__.py +79 -0
  80. aury/boot/domain/exceptions/__init__.py +132 -0
  81. aury/boot/domain/models/__init__.py +51 -0
  82. aury/boot/domain/models/base.py +69 -0
  83. aury/boot/domain/models/mixins.py +135 -0
  84. aury/boot/domain/models/models.py +96 -0
  85. aury/boot/domain/pagination/__init__.py +279 -0
  86. aury/boot/domain/repository/__init__.py +23 -0
  87. aury/boot/domain/repository/impl.py +423 -0
  88. aury/boot/domain/repository/interceptors.py +47 -0
  89. aury/boot/domain/repository/interface.py +106 -0
  90. aury/boot/domain/repository/query_builder.py +348 -0
  91. aury/boot/domain/service/__init__.py +11 -0
  92. aury/boot/domain/service/base.py +73 -0
  93. aury/boot/domain/transaction/__init__.py +404 -0
  94. aury/boot/infrastructure/__init__.py +104 -0
  95. aury/boot/infrastructure/cache/__init__.py +31 -0
  96. aury/boot/infrastructure/cache/backends.py +348 -0
  97. aury/boot/infrastructure/cache/base.py +68 -0
  98. aury/boot/infrastructure/cache/exceptions.py +37 -0
  99. aury/boot/infrastructure/cache/factory.py +94 -0
  100. aury/boot/infrastructure/cache/manager.py +274 -0
  101. aury/boot/infrastructure/database/__init__.py +39 -0
  102. aury/boot/infrastructure/database/config.py +71 -0
  103. aury/boot/infrastructure/database/exceptions.py +44 -0
  104. aury/boot/infrastructure/database/manager.py +317 -0
  105. aury/boot/infrastructure/database/query_tools/__init__.py +164 -0
  106. aury/boot/infrastructure/database/strategies/__init__.py +198 -0
  107. aury/boot/infrastructure/di/__init__.py +15 -0
  108. aury/boot/infrastructure/di/container.py +393 -0
  109. aury/boot/infrastructure/events/__init__.py +33 -0
  110. aury/boot/infrastructure/events/bus.py +362 -0
  111. aury/boot/infrastructure/events/config.py +52 -0
  112. aury/boot/infrastructure/events/consumer.py +134 -0
  113. aury/boot/infrastructure/events/middleware.py +51 -0
  114. aury/boot/infrastructure/events/models.py +63 -0
  115. aury/boot/infrastructure/monitoring/__init__.py +529 -0
  116. aury/boot/infrastructure/scheduler/__init__.py +19 -0
  117. aury/boot/infrastructure/scheduler/exceptions.py +37 -0
  118. aury/boot/infrastructure/scheduler/manager.py +478 -0
  119. aury/boot/infrastructure/storage/__init__.py +38 -0
  120. aury/boot/infrastructure/storage/base.py +164 -0
  121. aury/boot/infrastructure/storage/exceptions.py +37 -0
  122. aury/boot/infrastructure/storage/factory.py +88 -0
  123. aury/boot/infrastructure/tasks/__init__.py +24 -0
  124. aury/boot/infrastructure/tasks/config.py +45 -0
  125. aury/boot/infrastructure/tasks/constants.py +37 -0
  126. aury/boot/infrastructure/tasks/exceptions.py +37 -0
  127. aury/boot/infrastructure/tasks/manager.py +490 -0
  128. aury/boot/testing/__init__.py +24 -0
  129. aury/boot/testing/base.py +122 -0
  130. aury/boot/testing/client.py +163 -0
  131. aury/boot/testing/factory.py +154 -0
  132. aury/boot/toolkit/__init__.py +21 -0
  133. aury/boot/toolkit/http/__init__.py +367 -0
  134. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/METADATA +3 -2
  135. aury_boot-0.0.3.dist-info/RECORD +137 -0
  136. aury_boot-0.0.2.dist-info/RECORD +0 -5
  137. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/WHEEL +0 -0
  138. {aury_boot-0.0.2.dist-info → aury_boot-0.0.3.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,163 @@
1
+ """测试客户端。
2
+
3
+ 封装 FastAPI TestClient,提供便捷的测试接口。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, Optional
9
+
10
+ from fastapi import FastAPI
11
+ from fastapi.testclient import TestClient as FastAPITestClient
12
+ from httpx import Response
13
+
14
+ from aury.boot.common.logging import logger
15
+
16
+
17
+ class TestClient:
18
+ """测试客户端。
19
+
20
+ 封装 FastAPI TestClient,提供便捷的测试接口。
21
+
22
+ 使用示例:
23
+ app = FastAPI()
24
+ client = TestClient(app)
25
+
26
+ # GET 请求
27
+ response = await client.get("/users", headers={"Authorization": "Bearer token"})
28
+ assert response.status_code == 200
29
+
30
+ # POST 请求
31
+ response = await client.post("/users", json={"name": "张三"})
32
+ assert response.status_code == 201
33
+ assert response.json()["name"] == "张三"
34
+ """
35
+
36
+ def __init__(
37
+ self,
38
+ app: FastAPI,
39
+ base_url: str = "http://test",
40
+ headers: dict[str, str] | None = None,
41
+ ) -> None:
42
+ """初始化测试客户端。
43
+
44
+ Args:
45
+ app: FastAPI 应用实例
46
+ base_url: 基础URL(默认 "http://test")
47
+ headers: 默认请求头
48
+ """
49
+ self._client = FastAPITestClient(app, base_url=base_url)
50
+ self._default_headers = headers or {}
51
+ logger.debug("测试客户端已创建")
52
+
53
+ def _prepare_headers(self, headers: dict[str, str] | None = None) -> dict[str, str]:
54
+ """准备请求头。
55
+
56
+ Args:
57
+ headers: 额外的请求头
58
+
59
+ Returns:
60
+ dict[str, str]: 合并后的请求头
61
+ """
62
+ merged_headers = self._default_headers.copy()
63
+ if headers:
64
+ merged_headers.update(headers)
65
+ return merged_headers
66
+
67
+ def get(
68
+ self,
69
+ url: str,
70
+ params: dict[str, Any] | None = None,
71
+ headers: dict[str, str] | None = None,
72
+ ) -> Response:
73
+ """发送 GET 请求。
74
+
75
+ Args:
76
+ url: 请求URL
77
+ params: URL参数
78
+ headers: 请求头
79
+
80
+ Returns:
81
+ Response: HTTP响应
82
+ """
83
+ logger.debug(f"GET {url}")
84
+ return self._client.get(url, params=params, headers=self._prepare_headers(headers))
85
+
86
+ def post(
87
+ self,
88
+ url: str,
89
+ json: dict[str, Any] | None = None,
90
+ data: dict[str, Any] | None = None,
91
+ headers: dict[str, str] | None = None,
92
+ ) -> Response:
93
+ """发送 POST 请求。
94
+
95
+ Args:
96
+ url: 请求URL
97
+ json: JSON数据
98
+ data: 表单数据
99
+ headers: 请求头
100
+
101
+ Returns:
102
+ Response: HTTP响应
103
+ """
104
+ logger.debug(f"POST {url}")
105
+ if json:
106
+ return self._client.post(url, json=json, headers=self._prepare_headers(headers))
107
+ else:
108
+ return self._client.post(url, data=data, headers=self._prepare_headers(headers))
109
+
110
+ def put(
111
+ self,
112
+ url: str,
113
+ json: dict[str, Any] | None = None,
114
+ headers: dict[str, str] | None = None,
115
+ ) -> Response:
116
+ """发送 PUT 请求。
117
+
118
+ Args:
119
+ url: 请求URL
120
+ json: JSON数据
121
+ headers: 请求头
122
+
123
+ Returns:
124
+ Response: HTTP响应
125
+ """
126
+ logger.debug(f"PUT {url}")
127
+ return self._client.put(url, json=json, headers=self._prepare_headers(headers))
128
+
129
+ def delete(
130
+ self,
131
+ url: str,
132
+ headers: dict[str, str] | None = None,
133
+ ) -> Response:
134
+ """发送 DELETE 请求。
135
+
136
+ Args:
137
+ url: 请求URL
138
+ headers: 请求头
139
+
140
+ Returns:
141
+ Response: HTTP响应
142
+ """
143
+ logger.debug(f"DELETE {url}")
144
+ return self._client.delete(url, headers=self._prepare_headers(headers))
145
+
146
+ def patch(
147
+ self,
148
+ url: str,
149
+ json: dict[str, Any] | None = None,
150
+ headers: dict[str, str] | None = None,
151
+ ) -> Response:
152
+ """发送 PATCH 请求。
153
+
154
+ Args:
155
+ url: 请求URL
156
+ json: JSON数据
157
+ headers: 请求头
158
+
159
+ Returns:
160
+ Response: HTTP响应
161
+ """
162
+ logger.debug(f"PATCH {url}")
163
+ return self._client.patch(url, json=json, headers=self._prepare_headers(headers))
@@ -0,0 +1,154 @@
1
+ """测试数据工厂。
2
+
3
+ 提供便捷的测试数据生成工具,类似 Django 的 Factory。
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ from typing import Any, TypeVar
9
+
10
+ from faker import Faker
11
+
12
+ from aury.boot.common.logging import logger
13
+ from aury.boot.domain.models import Base
14
+
15
+ ModelType = TypeVar("ModelType", bound=Base)
16
+
17
+ # 全局 Faker 实例
18
+ _faker = Faker(["zh_CN", "en_US"])
19
+
20
+
21
+ class Factory:
22
+ """测试数据工厂。
23
+
24
+ 提供便捷的测试数据生成工具。
25
+
26
+ 使用示例:
27
+ # 创建工厂
28
+ user_factory = Factory(User)
29
+
30
+ # 创建单个实例
31
+ user = await user_factory.create(name="张三", email="zhangsan@example.com")
32
+
33
+ # 创建多个实例
34
+ users = await user_factory.create_batch(5, name="批量用户")
35
+
36
+ # 使用 Faker 生成随机数据
37
+ user = await user_factory.create() # 使用默认值或随机生成
38
+ """
39
+
40
+ def __init__(self, model_class: type[ModelType], **defaults: Any) -> None:
41
+ """初始化工厂。
42
+
43
+ Args:
44
+ model_class: 模型类
45
+ **defaults: 默认属性值
46
+ """
47
+ self._model_class = model_class
48
+ self._defaults = defaults
49
+ logger.debug(f"创建工厂: {model_class.__name__}")
50
+
51
+ def _generate_field_value(self, field_name: str, field_type: Any) -> Any:
52
+ """生成字段值。
53
+
54
+ Args:
55
+ field_name: 字段名
56
+ field_type: 字段类型
57
+
58
+ Returns:
59
+ Any: 生成的字段值
60
+ """
61
+ # 根据字段名和类型生成合适的值
62
+ if "email" in field_name.lower():
63
+ return _faker.email()
64
+ elif "name" in field_name.lower() or "username" in field_name.lower():
65
+ return _faker.name()
66
+ elif "phone" in field_name.lower() or "mobile" in field_name.lower():
67
+ return _faker.phone_number()
68
+ elif "address" in field_name.lower():
69
+ return _faker.address()
70
+ elif "url" in field_name.lower():
71
+ return _faker.url()
72
+ elif "text" in field_name.lower() or "content" in field_name.lower():
73
+ return _faker.text()
74
+ elif "date" in field_name.lower() or "time" in field_name.lower():
75
+ return _faker.date_time()
76
+ elif "int" in str(field_type).lower() or "integer" in str(field_type).lower():
77
+ return _faker.random_int()
78
+ elif "float" in str(field_type).lower():
79
+ return _faker.pyfloat()
80
+ elif "bool" in str(field_type).lower() or "boolean" in str(field_type).lower():
81
+ return _faker.boolean()
82
+ else:
83
+ return _faker.word()
84
+
85
+ def _build_attributes(self, **overrides: Any) -> dict[str, Any]:
86
+ """构建属性字典。
87
+
88
+ Args:
89
+ **overrides: 覆盖的属性值
90
+
91
+ Returns:
92
+ dict[str, Any]: 属性字典
93
+ """
94
+ attrs = self._defaults.copy()
95
+
96
+ # 如果模型有字段定义,尝试生成缺失的字段
97
+ if hasattr(self._model_class, "__table__"):
98
+ table = self._model_class.__table__
99
+ for column in table.columns:
100
+ if column.name not in attrs and column.name not in overrides:
101
+ # 跳过主键和自动生成的字段
102
+ if column.primary_key or column.server_default:
103
+ continue
104
+ # 尝试生成值
105
+ attrs[column.name] = self._generate_field_value(column.name, column.type)
106
+
107
+ # 应用覆盖值
108
+ attrs.update(overrides)
109
+ return attrs
110
+
111
+ async def create(self, **kwargs: Any) -> ModelType:
112
+ """创建单个实例。
113
+
114
+ Args:
115
+ **kwargs: 属性值(覆盖默认值)
116
+
117
+ Returns:
118
+ ModelType: 创建的模型实例
119
+ """
120
+ attrs = self._build_attributes(**kwargs)
121
+ instance = self._model_class(**attrs)
122
+ logger.debug(f"创建测试数据: {self._model_class.__name__}")
123
+ return instance
124
+
125
+ async def create_batch(self, size: int, **kwargs: Any) -> list[ModelType]:
126
+ """批量创建实例。
127
+
128
+ Args:
129
+ size: 创建数量
130
+ **kwargs: 属性值(覆盖默认值)
131
+
132
+ Returns:
133
+ list[ModelType]: 创建的模型实例列表
134
+ """
135
+ instances = []
136
+ for _ in range(size):
137
+ instance = await self.create(**kwargs)
138
+ instances.append(instance)
139
+ logger.debug(f"批量创建 {size} 个测试数据: {self._model_class.__name__}")
140
+ return instances
141
+
142
+ def build(self, **kwargs: Any) -> ModelType:
143
+ """构建实例(不保存到数据库)。
144
+
145
+ Args:
146
+ **kwargs: 属性值(覆盖默认值)
147
+
148
+ Returns:
149
+ ModelType: 构建的模型实例
150
+ """
151
+ attrs = self._build_attributes(**kwargs)
152
+ instance = self._model_class(**attrs)
153
+ logger.debug(f"构建测试数据(未保存): {self._model_class.__name__}")
154
+ return instance
@@ -0,0 +1,21 @@
1
+ """工具包模块。
2
+
3
+ 提供各种实用工具,如 HTTP 客户端等。
4
+ """
5
+
6
+ from .http import (
7
+ HttpClient,
8
+ HttpClientConfig,
9
+ LoggingInterceptor,
10
+ RequestInterceptor,
11
+ RetryConfig,
12
+ )
13
+
14
+ __all__ = [
15
+ "HttpClient",
16
+ "HttpClientConfig",
17
+ "LoggingInterceptor",
18
+ "RequestInterceptor",
19
+ "RetryConfig",
20
+ ]
21
+
@@ -0,0 +1,367 @@
1
+ """HTTP客户端 - 企业级HTTP请求工具。
2
+
3
+ 特性:
4
+ - 连接池管理
5
+ - 自动重试机制(使用tenacity)
6
+ - 请求/响应拦截器
7
+ - 超时控制
8
+ - 错误处理
9
+ - 请求日志
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ from abc import ABC, abstractmethod
15
+ from typing import Any, Optional, TypeVar
16
+
17
+ import httpx
18
+ from pydantic import BaseModel
19
+ from tenacity import (
20
+ retry,
21
+ retry_if_exception_type,
22
+ stop_after_attempt,
23
+ wait_exponential,
24
+ )
25
+
26
+ from aury.boot.common.logging import logger
27
+
28
+ T = TypeVar("T")
29
+
30
+
31
+ class RetryConfig(BaseModel):
32
+ """重试配置(Pydantic)。"""
33
+
34
+ max_retries: int = 3
35
+ retry_delay: float = 1.0
36
+ backoff_factor: float = 2.0
37
+ retry_on_status: list[int] = [500, 502, 503, 504]
38
+ retry_on_exceptions: tuple = (httpx.TimeoutException, httpx.NetworkError)
39
+
40
+
41
+ class HttpClientConfig(BaseModel):
42
+ """HTTP客户端配置(Pydantic)。"""
43
+
44
+ base_url: str = ""
45
+ timeout: float = 30.0
46
+ follow_redirects: bool = True
47
+ max_connections: int = 100
48
+ max_keepalive_connections: int = 20
49
+ keepalive_expiry: float = 5.0
50
+ retry: RetryConfig | None = None
51
+
52
+
53
+ class RequestInterceptor(ABC):
54
+ """请求拦截器接口。"""
55
+
56
+ @abstractmethod
57
+ async def before_request(self, request: httpx.Request) -> httpx.Request:
58
+ """请求前处理。"""
59
+ pass
60
+
61
+ @abstractmethod
62
+ async def after_response(self, response: httpx.Response) -> httpx.Response:
63
+ """响应后处理。"""
64
+ pass
65
+
66
+
67
+ class LoggingInterceptor(RequestInterceptor):
68
+ """日志拦截器。"""
69
+
70
+ async def before_request(self, request: httpx.Request) -> httpx.Request:
71
+ """记录请求日志。"""
72
+ logger.debug(
73
+ f"HTTP请求: {request.method} {request.url} | "
74
+ f"Headers: {dict(request.headers)}"
75
+ )
76
+ return request
77
+
78
+ async def after_response(self, response: httpx.Response) -> httpx.Response:
79
+ """记录响应日志。"""
80
+ logger.debug(
81
+ f"HTTP响应: {response.status_code} {response.url} | "
82
+ f"耗时: {response.elapsed.total_seconds():.3f}s"
83
+ )
84
+ return response
85
+
86
+
87
+ class HttpClient:
88
+ """企业级HTTP客户端。
89
+
90
+ 特性:
91
+ - 连接池管理
92
+ - 自动重试(使用tenacity)
93
+ - 拦截器支持
94
+ - 超时控制
95
+ - 错误处理
96
+
97
+ 使用示例:
98
+ # 基础使用
99
+ client = HttpClient(base_url="https://api.example.com")
100
+ response = await client.get("/users")
101
+
102
+ # 带重试
103
+ config = HttpClientConfig(
104
+ base_url="https://api.example.com",
105
+ retry=RetryConfig(max_retries=3)
106
+ )
107
+ client = HttpClient.from_config(config)
108
+ response = await client.get("/users")
109
+
110
+ # 添加拦截器
111
+ client.add_interceptor(LoggingInterceptor())
112
+ """
113
+
114
+ def __init__(
115
+ self,
116
+ base_url: str = "",
117
+ *,
118
+ timeout: float = 30.0,
119
+ follow_redirects: bool = True,
120
+ max_connections: int = 100,
121
+ retry_config: RetryConfig | None = None,
122
+ ) -> None:
123
+ """初始化HTTP客户端。
124
+
125
+ Args:
126
+ base_url: 基础URL
127
+ timeout: 超时时间(秒)
128
+ follow_redirects: 是否跟随重定向
129
+ max_connections: 最大连接数
130
+ retry_config: 重试配置
131
+ """
132
+ self._base_url = base_url
133
+ self._timeout = timeout
134
+ self._follow_redirects = follow_redirects
135
+ self._max_connections = max_connections
136
+ self._retry_config = retry_config or RetryConfig()
137
+ self._interceptors: list[RequestInterceptor] = []
138
+
139
+ # 创建HTTP客户端(使用连接池)
140
+ limits = httpx.Limits(
141
+ max_connections=max_connections,
142
+ max_keepalive_connections=20,
143
+ keepalive_expiry=5.0,
144
+ )
145
+
146
+ self._client = httpx.AsyncClient(
147
+ base_url=base_url,
148
+ timeout=timeout,
149
+ follow_redirects=follow_redirects,
150
+ limits=limits,
151
+ )
152
+
153
+ logger.debug(f"HTTP客户端初始化: base_url={base_url}, timeout={timeout}")
154
+
155
+ @classmethod
156
+ def from_config(cls, config: HttpClientConfig) -> HttpClient:
157
+ """从配置创建客户端。
158
+
159
+ Args:
160
+ config: 客户端配置
161
+
162
+ Returns:
163
+ HttpClient: HTTP客户端实例
164
+ """
165
+ return cls(
166
+ base_url=config.base_url,
167
+ timeout=config.timeout,
168
+ follow_redirects=config.follow_redirects,
169
+ max_connections=config.max_connections,
170
+ retry_config=config.retry,
171
+ )
172
+
173
+ def add_interceptor(self, interceptor: RequestInterceptor) -> None:
174
+ """添加拦截器。
175
+
176
+ Args:
177
+ interceptor: 拦截器实例
178
+ """
179
+ self._interceptors.append(interceptor)
180
+ logger.debug(f"添加拦截器: {interceptor.__class__.__name__}")
181
+
182
+ async def _apply_interceptors(
183
+ self,
184
+ request: httpx.Request,
185
+ response: httpx.Response | None = None,
186
+ ) -> tuple[httpx.Request, httpx.Response | None]:
187
+ """应用拦截器。"""
188
+ # 请求前拦截
189
+ for interceptor in self._interceptors:
190
+ request = await interceptor.before_request(request)
191
+
192
+ # 响应后拦截
193
+ if response:
194
+ for interceptor in self._interceptors:
195
+ response = await interceptor.after_response(response)
196
+
197
+ return request, response
198
+
199
+ async def _make_request(
200
+ self,
201
+ method: str,
202
+ url: str,
203
+ **kwargs: Any,
204
+ ) -> httpx.Response:
205
+ """执行HTTP请求(内部方法,使用tenacity重试)。"""
206
+ response = await self._client.request(method, url, **kwargs)
207
+
208
+ # 检查状态码是否需要重试
209
+ if response.status_code in self._retry_config.retry_on_status:
210
+ logger.warning(
211
+ f"请求失败,状态码: {response.status_code}, 将重试"
212
+ )
213
+ raise httpx.HTTPStatusError(
214
+ f"HTTP {response.status_code}",
215
+ request=response.request,
216
+ response=response,
217
+ )
218
+
219
+ return response
220
+
221
+ def _get_retry_decorator(self):
222
+ """动态构建重试装饰器。"""
223
+ return retry(
224
+ stop=stop_after_attempt(self._retry_config.max_retries + 1),
225
+ wait=wait_exponential(
226
+ multiplier=self._retry_config.retry_delay,
227
+ min=self._retry_config.retry_delay,
228
+ max=self._retry_config.retry_delay * (self._retry_config.backoff_factor ** self._retry_config.max_retries),
229
+ ),
230
+ retry=retry_if_exception_type(*self._retry_config.retry_on_exceptions),
231
+ reraise=True,
232
+ )
233
+
234
+ async def request(
235
+ self,
236
+ method: str,
237
+ url: str,
238
+ *,
239
+ headers: dict[str, str] | None = None,
240
+ params: dict[str, Any] | None = None,
241
+ json: Any = None,
242
+ data: Any = None,
243
+ files: Any = None,
244
+ **kwargs: Any,
245
+ ) -> httpx.Response:
246
+ """发送HTTP请求。
247
+
248
+ Args:
249
+ method: HTTP方法
250
+ url: 请求URL
251
+ headers: 请求头
252
+ params: 查询参数
253
+ json: JSON数据
254
+ data: 表单数据
255
+ files: 文件
256
+ **kwargs: 其他参数
257
+
258
+ Returns:
259
+ httpx.Response: 响应对象
260
+
261
+ Raises:
262
+ httpx.HTTPError: 请求失败
263
+ """
264
+ # 构建完整URL
265
+ full_url = f"{self._base_url}{url}" if self._base_url else url
266
+
267
+ # 创建请求对象
268
+ request = self._client.build_request(
269
+ method=method,
270
+ url=full_url,
271
+ headers=headers,
272
+ params=params,
273
+ json=json,
274
+ data=data,
275
+ files=files,
276
+ **kwargs,
277
+ )
278
+
279
+ # 应用拦截器(请求前)
280
+ request, _ = await self._apply_interceptors(request)
281
+
282
+ try:
283
+ # 使用tenacity重试装饰器
284
+ retry_decorator = self._get_retry_decorator()
285
+
286
+ @retry_decorator
287
+ async def _execute_request():
288
+ return await self._make_request(
289
+ method=request.method,
290
+ url=str(request.url),
291
+ headers=request.headers,
292
+ content=request.content,
293
+ **kwargs,
294
+ )
295
+
296
+ response = await _execute_request()
297
+
298
+ # 应用拦截器(响应后)
299
+ _, response = await self._apply_interceptors(request, response)
300
+
301
+ # 检查状态码
302
+ response.raise_for_status()
303
+
304
+ return response
305
+
306
+ except httpx.HTTPError as exc:
307
+ logger.error(
308
+ f"HTTP请求失败: {method} {full_url} | "
309
+ f"错误: {type(exc).__name__}: {exc}"
310
+ )
311
+ raise
312
+
313
+ async def get(self, url: str, **kwargs: Any) -> httpx.Response:
314
+ """GET请求。"""
315
+ return await self.request("GET", url, **kwargs)
316
+
317
+ async def post(self, url: str, **kwargs: Any) -> httpx.Response:
318
+ """POST请求。"""
319
+ return await self.request("POST", url, **kwargs)
320
+
321
+ async def put(self, url: str, **kwargs: Any) -> httpx.Response:
322
+ """PUT请求。"""
323
+ return await self.request("PUT", url, **kwargs)
324
+
325
+ async def patch(self, url: str, **kwargs: Any) -> httpx.Response:
326
+ """PATCH请求。"""
327
+ return await self.request("PATCH", url, **kwargs)
328
+
329
+ async def delete(self, url: str, **kwargs: Any) -> httpx.Response:
330
+ """DELETE请求。"""
331
+ return await self.request("DELETE", url, **kwargs)
332
+
333
+ async def head(self, url: str, **kwargs: Any) -> httpx.Response:
334
+ """HEAD请求。"""
335
+ return await self.request("HEAD", url, **kwargs)
336
+
337
+ async def options(self, url: str, **kwargs: Any) -> httpx.Response:
338
+ """OPTIONS请求。"""
339
+ return await self.request("OPTIONS", url, **kwargs)
340
+
341
+ async def close(self) -> None:
342
+ """关闭客户端。"""
343
+ await self._client.aclose()
344
+ logger.debug("HTTP客户端已关闭")
345
+
346
+ async def __aenter__(self) -> HttpClient:
347
+ """异步上下文管理器入口。"""
348
+ return self
349
+
350
+ async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
351
+ """异步上下文管理器出口。"""
352
+ await self.close()
353
+
354
+ def __repr__(self) -> str:
355
+ """字符串表示。"""
356
+ return f"<HttpClient base_url={self._base_url} timeout={self._timeout}>"
357
+
358
+
359
+ __all__ = [
360
+ "HttpClient",
361
+ "HttpClientConfig",
362
+ "LoggingInterceptor",
363
+ "RequestInterceptor",
364
+ "RetryConfig",
365
+ ]
366
+
367
+