pythonboot 0.1.0__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.
@@ -0,0 +1,59 @@
1
+ """Container access utilities for the current execution context."""
2
+
3
+ from collections.abc import Iterator
4
+ from contextlib import contextmanager
5
+ from contextvars import ContextVar
6
+ from typing import Any, TypeVar
7
+
8
+ from pythonboot.context import ApplicationContext
9
+ from pythonboot.exceptions import ContainerStateError
10
+
11
+ T = TypeVar("T")
12
+ _current_context: ContextVar[ApplicationContext | None] = ContextVar(
13
+ "pythonboot_application_context", default=None,
14
+ )
15
+
16
+
17
+ class ContextUtil:
18
+ @staticmethod
19
+ def get_application_context() -> ApplicationContext:
20
+ context = _current_context.get()
21
+ if context is None:
22
+ raise ContainerStateError(
23
+ "No ApplicationContext is bound to the current execution context; "
24
+ "use this utility during requests or application lifespan, "
25
+ "or bind standalone tasks with ContextUtil.use(context)"
26
+ )
27
+ return context
28
+
29
+ @staticmethod
30
+ def get_bean(
31
+ bean: type[T] | str, *, name: str | None = None, qualifier: str | None = None,
32
+ ) -> Any:
33
+ return ContextUtil.get_application_context().get_bean(bean, name=name, qualifier=qualifier)
34
+
35
+ @staticmethod
36
+ def get_bean_by_name(name: str) -> Any:
37
+ return ContextUtil.get_bean(name)
38
+
39
+ @staticmethod
40
+ def get_bean_by_type(bean_type: type[T], *, qualifier: str | None = None) -> T:
41
+ return ContextUtil.get_bean(bean_type, qualifier=qualifier)
42
+
43
+ @staticmethod
44
+ async def aget_bean(
45
+ bean: type[T] | str, *, name: str | None = None, qualifier: str | None = None,
46
+ ) -> Any:
47
+ return await ContextUtil.get_application_context().aget_bean(bean, name=name, qualifier=qualifier)
48
+
49
+ @staticmethod
50
+ @contextmanager
51
+ def use(context: ApplicationContext) -> Iterator[ApplicationContext]:
52
+ """Temporarily bind a container and restore the previous context on exit."""
53
+ if not isinstance(context, ApplicationContext):
54
+ raise TypeError("context must be an ApplicationContext")
55
+ token = _current_context.set(context)
56
+ try:
57
+ yield context
58
+ finally:
59
+ _current_context.reset(token)
@@ -0,0 +1,502 @@
1
+ Metadata-Version: 2.5
2
+ Name: pythonboot
3
+ Version: 0.1.0
4
+ Summary: A Spring Boot-inspired Python application core with IoC, AOP, and lifecycle management
5
+ Requires-Python: >=3.12
6
+ Requires-Dist: pydantic-settings<3,>=2.7
7
+ Requires-Dist: pydantic<3,>=2.10
8
+ Requires-Dist: pyyaml<7,>=6
9
+ Requires-Dist: wrapt<3,>=1.17
10
+ Description-Content-Type: text/markdown
11
+
12
+ # PythonBoot
13
+
14
+ [English](README.en.md) | 中文
15
+
16
+ 借鉴 Spring Boot 的 Python 后端框架,核心容器与 FastAPI Web 实现独立发布。
17
+ 提供启动组合装饰器、Bean 扫描、单例/多例 IoC 容器、构造器及属性注入、生命周期管理和 AOP。
18
+
19
+ 要求 Python >=3.12。依赖与开发规范见 [AGENTS.md](AGENTS.md),
20
+ 模块职责见 [架构文档](docs/architecture.md)。
21
+
22
+ ## 发布
23
+
24
+ 复制本地发布配置文件并填入 PyPI 令牌,再执行发布脚本。配置文件不会提交到 Git;脚本会确认工作区干净、构建分发包并上传至 PyPI:
25
+
26
+ ```bash
27
+ cp scripts/publish.env.example scripts/.publish.env
28
+ # 编辑 scripts/.publish.env,填写 UV_PUBLISH_TOKEN
29
+ ./scripts/publish.sh
30
+ ```
31
+
32
+ 核心安装 `pythonboot`;Web 应用安装 `pythonboot-starter-web`,自动引入
33
+ `pythonboot-web`、核心、FastAPI 和 Uvicorn。核心接口从 `pythonboot` 导入,Web 与启动
34
+ 接口从 `pythonboot_web` 导入。实现位于同级 `PythonBootStarter/modules/pythonboot-web`。
35
+ 原来的 `pythonboot.web` 和核心顶层 Web 导出已移除,YAML 配置键保持不变。
36
+ 本地示例依赖两个同级仓库,`uv sync` 安装开发组;纯核心使用 `uv sync --no-dev`。
37
+
38
+ 项目采用 `src` 包布局,先安装项目,再启动示例:
39
+
40
+ ```bash
41
+ uv sync
42
+ uv run uvicorn main:app --reload
43
+ ```
44
+
45
+ `examples/` 约定为基于框架的样例后端项目,启动类位于
46
+ `examples/test_application.py`,继承 `PythonBootApplication`。
47
+ 启动类默认递归扫描自身所在包:
48
+
49
+ ```python
50
+ from pythonboot_web import PythonBootApplication, pythonboot_application
51
+
52
+ @pythonboot_application
53
+ class TestApplication(PythonBootApplication):
54
+ pass
55
+
56
+ app = TestApplication().app
57
+ ```
58
+
59
+ `@pythonboot_application()` 同样支持默认扫描。启动类位于
60
+ `examples.test_application` 时,默认扫描 `examples` 及所有子包、模块。
61
+ 扫描支持无 `__init__.py` 的目录,启动入口复用当前模块以避免重复创建应用。
62
+ 独立脚本没有所属包时,需要显式指定扫描范围。
63
+
64
+ 可通过 `@pythonboot_application(scan_packages=("a.**", "b.*"))` 覆盖默认范围,
65
+ 构造参数 `scan_packages` 优先于装饰器配置;空元组关闭扫描。
66
+ 普通包名 `a` 递归扫描该包,`a.*` 只匹配下一层包或模块(不递归进入),
67
+ `a.**` 匹配该包及任意深度的子包、模块,表达式可组合,例如 `a.*.service.**`。
68
+ 匹配仅在表达式指定的包路径内进行,不遍历整个 Python 环境。
69
+ 工厂 `create_app` 使用显式 `scan_packages`,默认不扫描。
70
+
71
+ `@component` / `@service` 声明单例组件,构造器通过类型注解注入依赖。
72
+ `@configuration` 声明配置组件,`@bean` 工厂方法通过返回类型注册 Bean。
73
+
74
+ ## 配置属性
75
+
76
+ 启动时通过 `config_dir` 指定配置目录,框架先读取 `application.yml`,再按其中的
77
+ `active.profile` 读取对应的 `application-{profile}.yml`;后读取的同层配置覆盖先读取的配置。
78
+ 配置类继承
79
+ `BaseSettings` 并使用 `@configuration_properties` 绑定 YAML 前缀,环境变量仍优先于 YAML。
80
+
81
+ 日志默认使用 `INFO` 级别及 Spring Boot 风格的控制台格式。可在 YAML 中设置根日志级别和指定 Logger 级别:
82
+
83
+ ```yaml
84
+ pythonboot:
85
+ logging:
86
+ level: INFO
87
+ colors: true
88
+ loggers:
89
+ examples: DEBUG
90
+ ```
91
+
92
+ 需要自定义 Handler、Formatter 或输出目的地时,可在 `logging` 节点提供标准 Python
93
+ `logging.config.dictConfig` 配置(必须包含 `version`)。所有 PythonBoot 框架配置均位于
94
+ `pythonboot` 前缀下。
95
+
96
+ ## API 文档
97
+
98
+ 默认在 `/docs` 本地嵌入全屏 Scalar API Client,使用应用的 `/openapi.json`。可通过 YAML 切换
99
+ 文档界面或在生产环境关闭文档与 OpenAPI:
100
+
101
+ ```yaml
102
+ pythonboot:
103
+ docs:
104
+ enabled: true
105
+ provider: scalar # scalar、swagger、redoc
106
+ path: /docs
107
+ openapi_path: /openapi.json
108
+ web:
109
+ datetime_format: "%Y-%m-%d %H:%M:%S"
110
+ ```
111
+
112
+ 将 `enabled` 设为 `false` 时,文档页面和 OpenAPI 端点均不会注册。
113
+ `scalar` 为默认 API Reference 文档页面。
114
+
115
+ Controller 的 `datetime` 路径、查询参数及 Pydantic 请求体字段默认按
116
+ `YYYY-MM-DD HH:mm:ss` 解析;通过 `pythonboot.web.datetime_format` 修改格式。
117
+ 响应 JSON 中的 `datetime` 也按相同格式输出。
118
+
119
+ 单个参数或请求体字段可覆盖全局格式:
120
+
121
+ ```python
122
+ from datetime import datetime
123
+ from typing import Annotated
124
+
125
+ from fastapi import Query
126
+ from pythonboot_web import date_time, get
127
+
128
+ @get("/report")
129
+ def report(
130
+ created_at: Annotated[datetime, date_time("%Y/%m/%d %H:%M:%S"), Query()],
131
+ ) -> dict:
132
+ return {"created_at": created_at}
133
+ ```
134
+
135
+ ```yaml
136
+ # application.yml
137
+ active:
138
+ profile: dev
139
+ ```
140
+
141
+ ```python
142
+ from pydantic_settings import BaseSettings
143
+ from pythonboot import configuration, configuration_properties
144
+ from pythonboot_web import PythonBootApplication, pythonboot_application
145
+
146
+ @configuration
147
+ @configuration_properties("health")
148
+ class HealthSettings(BaseSettings):
149
+ status: str = "ok"
150
+
151
+ @pythonboot_application(scan_packages=("myapp",))
152
+ class Application(PythonBootApplication):
153
+ pass
154
+
155
+ application = Application(config_dir="./config")
156
+ ```
157
+
158
+ `HealthSettings` 会作为单例 Bean 自动注册,可通过构造器或 `autowired()` 注入。
159
+ `@controller` 声明控制器类,通过构造器注入 Service;
160
+ 方法使用 `@get`、`@post`、`@put`、`@patch`、`@delete` 或 `@route` 声明路由。
161
+
162
+ ```python
163
+ from pythonboot import autowired, service
164
+ from pythonboot_web import controller, get
165
+
166
+ @service
167
+ class HealthService:
168
+ def health(self) -> dict[str, str]:
169
+ return {"status": "ok"}
170
+
171
+ @controller(prefix="/health", tags=["health"])
172
+ class HealthController:
173
+ health_service: HealthService = autowired()
174
+
175
+ @get("")
176
+ def health(self) -> dict[str, str]:
177
+ return self.health_service.health()
178
+ ```
179
+
180
+ 控制器路由支持同步和异步方法,参数与响应继续使用 FastAPI 的类型声明、
181
+ `Query`、`Body`、`Depends` 等机制,`self` 不暴露为 HTTP 参数。
182
+ 扫描模块中的原生 `APIRouter` 自动注册;路由通过
183
+ `Depends(inject(ServiceType))` 获取当前应用的 Bean,也可通过 `name` 指定候选。
184
+
185
+ Bean 在 ASGI lifespan 启动时实例化。声明组件或 Bean 时,可通过
186
+ `init_method="initialize"`、`destroy_method="shutdown"` 指定同步或异步生命周期方法。
187
+ Bean 工厂必须是同步函数,外部资源在初始化方法中创建,在销毁方法中释放。
188
+ 缺失依赖、重复名称、多个类型候选及无法解析的循环依赖都会明确报错。
189
+
190
+ 属性注入用法:
191
+
192
+ ```python
193
+ from pythonboot import autowired
194
+ from pythonboot_web import controller, get
195
+
196
+ @controller(prefix="/health")
197
+ class HealthController:
198
+ health_service: HealthService = autowired()
199
+
200
+ @get("")
201
+ def health(self) -> Health:
202
+ return self.health_service.health()
203
+ ```
204
+
205
+ 容器只注入标记 `autowired()` 的属性,支持继承的注入属性及按名称选择:
206
+ `health_service: HealthService = autowired(name="healthService")`。
207
+ `required=False` 允许缺失 Bean 时注入 None,属性类型建议写为 `Service | None`;
208
+ 重复候选或依赖 Bean 构造失败仍报错。
209
+
210
+ 属性在构造器执行后、初始化钩子执行前注入,不要在 `__init__` 中访问。
211
+ 没有 `__dict__` 的纯 slots 实例不支持属性注入。
212
+ Bean 在属性注入前已注册最终代理,因此支持 `A.b → B.a → A` 属性循环,
213
+ 双方拿到的仍是容器中的同一实例或 AOP 代理。纯构造器循环仍不支持,
214
+ 混合循环只有在被回指的 Bean 已完成构造时才能解析。
215
+
216
+ 懒加载同时支持 Bean 声明和属性注入:
217
+
218
+ ```python
219
+ from datetime import datetime
220
+
221
+ from pythonboot import autowired, component
222
+ from pythonboot_web import controller, get
223
+
224
+ @component(lazy=True)
225
+ class TimeProvider:
226
+ def __init__(self) -> None:
227
+ self.created_at = datetime.now().astimezone()
228
+
229
+ @controller(prefix="/lazy")
230
+ class TimeController:
231
+ time_provider: TimeProvider = autowired(lazy=True)
232
+
233
+ @get("/time")
234
+ def time(self) -> dict[str, datetime]:
235
+ return {"bean_time": self.time_provider.created_at}
236
+ ```
237
+
238
+ `autowired(lazy=True)` 首次读取属性时解析依赖,成功后缓存当前容器的最终实例/代理。
239
+ `@service(lazy=True)` 跳过启动时主动创建,也支持 `@component`、`@configuration`、
240
+ `@controller`、`@aspect` 及 `@bean`。要真正推迟创建,目标 Bean 需要声明 `lazy=True`,
241
+ 且不能被其他非延迟依赖提前引用。属性类型和 Bean 候选仍在注入时校验。
242
+ 并发首次获取在容器锁内创建,保持单例;延迟 Bean 创建后照常执行 AOP 和销毁。
243
+
244
+ 同步访问可执行同步初始化。延迟 Bean 使用 async 初始化钩子时,需先异步获取:
245
+
246
+ ```python
247
+ service = await request.app.state.context.aget_bean(HealthService)
248
+ ```
249
+
250
+ 异步获取会等待初始化完成;随后访问注入属性即可取得同一实例。
251
+ 未经异步初始化直接同步访问会明确报错,不会后台启动初始化或阻塞事件循环等待。
252
+ 声明懒加载不会自动解决纯构造器循环,也不要在构造或初始化过程中提前访问懒加载依赖。
253
+ 实际样例中的 `HealthService` 及其注入使用默认加载方式;懒加载仅用于时间 Bean 演示。
254
+
255
+ 时间单例演示:访问 `GET /lazy/time`。
256
+ `CurrentTime` 使用 `@component(lazy=True)` 注册单例组件,
257
+ Controller 通过 `current_time: CurrentTime = autowired(lazy=True)` 注入。
258
+ 第一次请求时才创建组件并记录时间;后续请求的 `bean_time` 保持不变,
259
+ `request_time` 随请求变化。重启应用后,首次请求会生成新的时间单例。
260
+
261
+ `GET /lazy/prototype-time` 使用 `@component(scope="prototype")` 时间组件,
262
+ 通过原生 `Depends(inject(PrototypeTime))` 每次请求重新获取,`bean_time` 随请求变化。
263
+
264
+ ## Bean 参数及作用域
265
+
266
+ `@component`、`@service`、`@configuration`、`@controller`、`@aspect`、`@bean`
267
+ 统一支持以下参数;`@bean` 用于工厂方法返回值,其他装饰器用于类。
268
+
269
+ | 参数 | 默认值 | 含义 |
270
+ | --- | --- | --- |
271
+ | `name` | 自动生成 | Bean 名称 |
272
+ | `scope` | `"singleton"` | `singleton` 应用级单例;`prototype` 每次获取创建 |
273
+ | `lazy` | `False` | 单例延迟创建;多例本身按需创建 |
274
+ | `primary` | `False` | 多候选时作为唯一首选 |
275
+ | `qualifiers` | `()` | 注入时使用的限定标签 |
276
+ | `depends_on` | `()` | 必须先初始化的 Bean 名称 |
277
+ | `init_method` | `None` | 实例初始化方法名 |
278
+ | `destroy_method` | `None` | 单例退出时的销毁方法名 |
279
+
280
+ ```python
281
+ @component(
282
+ name="cache", scope="singleton", lazy=True, primary=True,
283
+ qualifiers=("local",), depends_on=("settings",),
284
+ init_method="initialize", destroy_method="shutdown",
285
+ )
286
+ class Cache:
287
+ def initialize(self) -> None:
288
+ pass
289
+
290
+ def shutdown(self) -> None:
291
+ pass
292
+ ```
293
+
294
+ 限定符注入:`cache: Cache = autowired(qualifier="local")`。
295
+ 容器 `get_bean`、`aget_bean` 和 Web `inject` 同样支持 `name`、`qualifier`。
296
+ 构造器可使用 `Annotated[Cache, autowired(qualifier="local")]` 选择候选。
297
+ 限定符先过滤候选;存在多个候选时仅有一个 `primary=True` 才能自动选取,
298
+ 否则明确报错,不覆盖重复名称的 Bean。
299
+
300
+ `prototype` 每次 `context.get_bean(Type)` 或 `aget_bean(Type)` 创建新实例,
301
+ 运行依赖注入、AOP 和初始化,容器不缓存它,也不自动执行其销毁方法,
302
+ 资源由使用方负责释放。普通属性/构造器注入仅创建一次并保存该引用;
303
+ 懒属性在首次访问时创建并保存引用,并非每次访问都创建。
304
+ 通过 FastAPI `Depends(inject(Type))` 获取多例时,默认依赖缓存使同一请求复用,
305
+ 不同请求获取新实例;需要同一请求重复解析可指定 `use_cache=False`。
306
+ 多例不主动在启动时创建,纯多例循环依赖仍报错。
307
+
308
+ `depends_on` 按名称声明初始化顺序,缺失 Bean、声明循环或无法满足的初始化环
309
+ 都会明确报错;普通属性循环只能保证引用已注入,不保证循环双方都先完成初始化。
310
+ `@controller` 额外保留 `prefix`、`tags` 等原生路由参数,`@aspect` 额外支持 `order`。
311
+
312
+ `configure(app)` 仍可注册原生路由、中间件和异常处理器,构造参数仍支持
313
+ FastAPI 配置及原生 `lifespan`。用户 lifespan 在容器启动后进入,并在容器销毁前退出。
314
+
315
+ 样例项目按 `controller`、`service`、`config`、`domain`、`schema` 分层,
316
+ 其中 `schema` 专门存放数据库模型,当前尚未引入数据库依赖。
317
+
318
+ ## 初始化注解
319
+
320
+ ```python
321
+ from pythonboot import autowired, component, postconstruct
322
+
323
+ @component
324
+ class Cache:
325
+ settings: Settings = autowired()
326
+
327
+ @postconstruct
328
+ def initialize(self) -> None:
329
+ self.capacity = self.settings.cache_capacity
330
+ ```
331
+
332
+ `@postconstruct`(也支持 `@postconstruct()`)在依赖注入完成后自动调用,
333
+ 方法只能接收 self,支持同步或 async。单例每实例执行一次,多例每个新实例执行一次;
334
+ 懒加载 Bean 在首次创建时执行,异步回调仍需通过 `aget_bean` 等待。
335
+ 继承回调按基类到子类执行,同名覆盖遵循 Python 方法解析规则。
336
+ 多个回调按类中的声明顺序执行;先执行注解回调,再执行显式 init_method,
337
+ 同一方法同时被两种方式声明时只执行一次。回调不进入 AOP 通知链。
338
+ 时间演示组件的 `created_at` 已改为在 `@postconstruct` 中设置。
339
+
340
+ `@ondestroy`(也支持 `@ondestroy()`)在单例 Bean 销毁前调用,支持同步和 async。
341
+ 继承回调按子类到基类执行,再执行显式 `destroy_method`;同名方法只执行一次,
342
+ 且销毁回调不进入 AOP 通知链。prototype Bean 不自动执行销毁回调。
343
+
344
+ ## 容器工具
345
+
346
+ ```python
347
+ from pythonboot.util import ContextUtil
348
+
349
+ context = ContextUtil.get_application_context()
350
+ service = ContextUtil.get_bean_by_name("healthService")
351
+ service = ContextUtil.get_bean_by_type(HealthService)
352
+ # 同一接口也可按名称或类型获取:
353
+ service = ContextUtil.get_bean("healthService")
354
+ service = ContextUtil.get_bean(HealthService, qualifier="local")
355
+ ```
356
+
357
+ 请求、WebSocket 和应用 lifespan 内自动绑定当前应用容器,保持单例/多例、
358
+ 懒加载及 AOP 语义。异步初始化使用 `await ContextUtil.aget_bean(TypeOrName)`。
359
+ 独立任务使用 `with ContextUtil.use(application.context): ...` 显式绑定;
360
+ 未绑定时明确报错,不从其他应用获取 Bean。也支持 `from pythonboot import ContextUtil`。
361
+
362
+ Service 和 AOP 可通过 `RequestContextHolder` 获取当前 HTTP 请求:
363
+
364
+ ```python
365
+ from pythonboot import service
366
+ from pythonboot_web import RequestContextHolder
367
+
368
+ @service
369
+ class OrderService:
370
+ def current_user(self) -> str | None:
371
+ request = RequestContextHolder.get_request()
372
+ return request.headers.get("X-User-Id")
373
+ ```
374
+
375
+ 使用 `request.query_params`、`request.path_params` 获取查询和路径参数;异步方法中
376
+ 使用 `await request.json()` 获取 JSON 请求体。框架缓存已读取的请求体,避免重复消费。
377
+ 请求通过 `ContextVar` 隔离,支持异步接口及 FastAPI 的同步线程池接口;请求结束自动恢复
378
+ 绑定,请求外获取会报错。自行创建的线程不会自动继承上下文。
379
+
380
+ 请求上下文提供带锁的自定义数据 Map,每个请求独立创建,同步线程池与异步调用共享
381
+ 同一次请求的数据:
382
+
383
+ ```python
384
+ RequestContextHolder.putData("user_id", 123)
385
+ user_id = RequestContextHolder.getData("user_id")
386
+ value = RequestContextHolder.getData("missing", "default")
387
+ ```
388
+
389
+ Map 的读写操作线程安全,存入的可变对象需自行同步其内部修改。
390
+
391
+ 也可直接设置当前响应,响应头发送后不能再修改:
392
+
393
+ ```python
394
+ response = RequestContextHolder.get_response()
395
+ response.status_code = 201
396
+ response.set_header("X-Trace-Id", "abc123")
397
+ response.set_cookie("session", "token", httponly=True, secure=True)
398
+ response.redirect("/login")
399
+ ```
400
+
401
+ ## AOP
402
+
403
+ 切面类通过 `@aspect` 注册,支持构造器注入。
404
+
405
+ 业务方法可通过自定义注解选择切面,无需匹配包路径:
406
+
407
+ ```python
408
+ from pythonboot import JoinPoint, annotation, aspect, before, service
409
+
410
+ logged = annotation("logged")
411
+
412
+ @aspect
413
+ class LoggingAspect:
414
+ @before(logged)
415
+ def log(self, jp: JoinPoint) -> None:
416
+ print(jp.qualified_name)
417
+
418
+ @service
419
+ class OrderService:
420
+ @logged
421
+ def create_order(self) -> str:
422
+ return "created"
423
+ ```
424
+
425
+ 调用容器注入的 `OrderService.create_order()` 就会触发日志切面,
426
+ 未标记的方法不会匹配这个切点。所有通知装饰器均支持注解切点。
427
+ 同一方法可叠加多个注解;同名注解匹配同一切点,建议在单独模块统一定义。
428
+ 注解仅标记方法,不改变调用签名,仍通过容器代理执行通知。
429
+ 样例在 `examples/annotations/aop_annotations.py` 定义 `logged`,
430
+ `HealthService.health()` 使用 `@logged`,`LoggingAspect` 使用 `@before(logged)`。
431
+
432
+ ```python
433
+ from pythonboot import ProceedingJoinPoint, around, aspect
434
+
435
+ @aspect(order=0)
436
+ class ServiceAspect:
437
+ @around("examples.service.**")
438
+ def invoke(self, join_point: ProceedingJoinPoint):
439
+ result = join_point.proceed()
440
+ return result
441
+ ```
442
+
443
+ 切点匹配 `模块.类.方法` 全限定名:`examples.service.**` 匹配所有服务方法,
444
+ `examples.service.*.*.health` 匹配指定层级的 health 方法。
445
+ `*` 匹配一层,`**` 匹配任意层级。可复用 `pointcut("examples.service.**")`,
446
+ 也可传入 `lambda method: method.bean_name == "healthService"` 等谓词。
447
+
448
+ 通知参数约定:
449
+
450
+ | 通知 | 方法签名 | 执行时机 |
451
+ | --- | --- | --- |
452
+ | `@before(cut)` | `(self, join_point)` | 调用前 |
453
+ | `@after(cut)` | `(self, join_point)` | 退出时,无论成功或异常 |
454
+ | `@after_returning(cut)` | `(self, join_point, result)` | 成功返回后 |
455
+ | `@after_throwing(cut)` | `(self, join_point, error)` | 抛出异常后,继续传播异常 |
456
+ | `@around(cut)` | `(self, join_point)` | 包围调用,可修改参数、结果或处理异常 |
457
+
458
+ 普通通知的返回值被忽略,只有环绕通知的返回值作为调用结果。
459
+ `JoinPoint` 提供 `bean_name`、`target`、`method`、`args`、`kwargs` 及
460
+ `qualified_name`。环绕通知使用 `proceed()` 沿用参数,或 `proceed(*args, **kwargs)`
461
+ 替换全部参数,可调用多次或不调用以短路目标方法。
462
+
463
+ 异步目标的环绕通知通常写为:
464
+
465
+ ```python
466
+ @around("myapp.service.**")
467
+ async def invoke(self, join_point: ProceedingJoinPoint):
468
+ return await join_point.proceed()
469
+ ```
470
+
471
+ 异步目标支持同步及异步通知;同步目标只支持同步通知,禁止返回 awaitable。
472
+ 切面 `order` 优先,其次为通知装饰器的 `order`,数值越小越先进入,
473
+ 退出通知按嵌套链反向执行;相同顺序按 Bean 名、通知方法名稳定排序。
474
+ 异常通知观察其内层调用抛出的异常;通知自身抛出的异常也会向外传播。
475
+ 切面自身、生命周期方法、私有方法、静态/类方法不增强。
476
+ 匹配到生成器方法时明确报错。同一 Bean 内部 `self.method()` 调用不会触发 AOP,
477
+ 通过容器注入的其他 Bean 调用会触发。样例切面位于 `examples/config/logging_aspect.py`。
478
+
479
+ 也可以使用启动类启动:
480
+
481
+ ```bash
482
+ python -m examples.test_application
483
+ ```
484
+
485
+ 默认监听 `127.0.0.1:8000`,`run()` 支持设置 `host`、`port` 和 `log_level`。
486
+ 样例提供 `GET /health`。热重载使用 `uvicorn main:app --reload`。
487
+
488
+ ## 全局异常处理
489
+
490
+ 使用 `@controller_advise` 声明全局异常处理 Bean,`@exception_handler` 指定处理的异常类型或 HTTP 状态码。处理方法接收原生 FastAPI 的 `Request` 与异常对象,并返回响应:
491
+
492
+ ```python
493
+ from fastapi import Request
494
+ from fastapi.responses import JSONResponse
495
+ from pythonboot_web import controller_advise, exception_handler
496
+
497
+ @controller_advise
498
+ class GlobalExceptionHandler:
499
+ @exception_handler(ValueError)
500
+ def handle_value_error(self, request: Request, error: ValueError) -> JSONResponse:
501
+ return JSONResponse(status_code=400, content={"detail": str(error)})
502
+ ```
@@ -0,0 +1,34 @@
1
+ pythonboot/__init__.py,sha256=uRlCwrcN5V9R4b8id33gi3IfLYitDY-JTSC_YqnnIwM,2086
2
+ pythonboot/aop/__init__.py,sha256=MkxP-gJivJsYOFfIAC2x8JxPOQcU0Qa4bZpmuNwHGnE,533
3
+ pythonboot/aop/decorators.py,sha256=cRadAN81KAgUoGuwO-WLkifOqGF8nBiiCun25L1j7WE,2635
4
+ pythonboot/aop/joinpoint.py,sha256=t6MkIDcuMifZcVzwrlCKqKU-SNz10NcmeeTQDmkTAk8,895
5
+ pythonboot/aop/pointcut.py,sha256=SL4oqoaCH2JWiWpZbLZFnc_a7_H20AcZjQZit5EQUDY,2958
6
+ pythonboot/aop/proxy.py,sha256=ANqDqAF9ipkhVpTub9oB5eFSrQSJbLwFbV3_KONgiqI,8979
7
+ pythonboot/beans/__init__.py,sha256=j7OENhXYAIseS93CEW95NiaQs12nuY1ZD4SwSWNjn38,444
8
+ pythonboot/beans/conditions.py,sha256=11LakmIfajcMdD0Y4MfnEa34EciiDJYpc_mDLFhYBBg,4228
9
+ pythonboot/beans/definitions.py,sha256=RNl9cnCnV5YrZoxMOAQ5qL2_IcMMET2F7HquNMqmclw,3830
10
+ pythonboot/beans/lifecycle.py,sha256=er25oa9uDTtjJH7WMkW_2yMrCKZyVLkUEV7J4NAEpbo,3864
11
+ pythonboot/beans/scanner.py,sha256=nnedfn12F0ib0vnBMVLW0B-dacHBdoS_XvvJu6jsbJs,6962
12
+ pythonboot/boot/__init__.py,sha256=gElWChwVula1dqnZCgN-ExmwZ3ML-fRCuHANUuJgDyQ,140
13
+ pythonboot/boot/autoconfiguration.py,sha256=KL2z9Cm1cHdVL51UUOrHf8IPABS_C0AsZjivgjFm-hE,5485
14
+ pythonboot/boot/task_configuration.py,sha256=5CK4-Wd4XGN9xRPDJYNyqSooKJlr70gogliWusCAV3Q,1938
15
+ pythonboot/config/__init__.py,sha256=obx4ingHpCaTpCk8t68ysw_ldkS_ZdHuabmMwcBn_oc,353
16
+ pythonboot/config/logging.py,sha256=FkJTusLfPu9i_70YBAfDlQ5hGRufe066gl4GgQI6jPw,3860
17
+ pythonboot/config/properties.py,sha256=EXnwL5Q9u5x4ZQKJFGQ3WXiQbCwub4q81idULJteeKI,5738
18
+ pythonboot/context/__init__.py,sha256=N_F4mCdkd4fT2czJ3gl41UCnl3fILkO4nxfzAVASUJM,226
19
+ pythonboot/context/container.py,sha256=5A5jkHenzQ-oiRkCiJMP4v35aQ-IEOy2H0SXQzAmVb8,19721
20
+ pythonboot/context/injection.py,sha256=ReNQk1nwtgRunhm-EDpr-CzUBF7JieIxYfCrPZEmrDY,2160
21
+ pythonboot/exceptions/__init__.py,sha256=javPkH7TSLE5yqEykQV1Dq5_k1wFzcCLafeHoffO0s4,993
22
+ pythonboot/task/__init__.py,sha256=DqrhZxz7uDioLaHDQ9A2I0vod3KFVeRZ761wR6fdyO4,286
23
+ pythonboot/task/executor.py,sha256=E0OlYlC6POsqW3zhvKKBcFOT5bZ6thu7J3HKu6wrv5w,10600
24
+ pythonboot/transaction/__init__.py,sha256=Mct9zVDyTzxex_hTTg4NevPOfB4aCyrSkaYBu_-OCgk,1287
25
+ pythonboot/transaction/decorators.py,sha256=uNYAAkBfV12CiKq9FvSR8ZOTK0m6XAZTBErt4AS9PVc,1798
26
+ pythonboot/transaction/definition.py,sha256=TOygOL52jgbExsphl99H0hFYhSZORe_JuaT6B71s8CI,3478
27
+ pythonboot/transaction/exceptions.py,sha256=zNnhTSZKFdKyhDH70f8_2b8EAmmzT1JYBDGkbBZwcQk,937
28
+ pythonboot/transaction/interceptor.py,sha256=JSOVPLGhWYOataRMnsEpGzZkFqUEcWs40WmLeKYaaDc,3289
29
+ pythonboot/transaction/manager.py,sha256=gVS33RmJeBcdC43k48jtzoCUW4o9jivjZCH0xpo4R9E,4175
30
+ pythonboot/util/__init__.py,sha256=53PyS_B98DPZIeWEHJOabbuM9QrAETDcvmcsW0sPjKg,124
31
+ pythonboot/util/context_util.py,sha256=p1Chr5FWLf_XeY9SNJ037TeAeDJmKZMY6H-Lsi5l_Xw,2166
32
+ pythonboot-0.1.0.dist-info/METADATA,sha256=o2EKg1ktdsM1pyUadOa5zW-OPzwYSSWypCxLOl6ZBZg,20711
33
+ pythonboot-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
34
+ pythonboot-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.32.4
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any