python-ddd-framework 0.7.0__py3-none-any.whl → 0.7.1__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.
@@ -15,7 +15,13 @@ from starlette.middleware import Middleware
15
15
  from ..application import Application
16
16
  from ..application_services import ApplicationServiceValidationError
17
17
  from ..authorization import PermissionDeniedError, UnauthenticatedError
18
- from ..errors import BusinessError, FrameworkError, ResourceConflictError, ResourceNotFoundError
18
+ from ..errors import (
19
+ BusinessError,
20
+ FrameworkError,
21
+ InvalidApplicationStateError,
22
+ ResourceConflictError,
23
+ ResourceNotFoundError,
24
+ )
19
25
  from ..lifecycle import ApplicationState
20
26
  from .contracts import RouterCatalog
21
27
  from .error_handlers import (
@@ -30,6 +36,7 @@ from .error_handlers import (
30
36
  )
31
37
  from .errors import FastApiAdapterError
32
38
  from .filters import ServiceFilter, _validate_filters
39
+ from .health import _APPLICATION_STATE_ATTRIBUTE
33
40
  from .middleware import DiMiddleware, _compile_middlewares
34
41
  from .module import FastApiModule
35
42
  from .realtime.runtime import _FastApiRealtimeRuntime
@@ -174,6 +181,7 @@ class FastApiAdapter:
174
181
  app.state.python_ddd_framework_filters = self._filters
175
182
  app.state.python_ddd_framework_upload_limits = self._upload_limits
176
183
  app.state.dishka_container = self._application._container_for_integration()
184
+ setattr(app.state, _APPLICATION_STATE_ATTRIBUTE, self._application)
177
185
  app.user_middleware = [*middlewares, Middleware(_RequestTaskMiddleware)]
178
186
  app.add_middleware(_RequestContextMiddleware, application=self._application)
179
187
  _install_tracing(self._application, app)
@@ -191,17 +199,19 @@ class FastApiAdapter:
191
199
  try:
192
200
  self._validate_start_target(app)
193
201
  except BaseException:
194
- if self._installed_app is app:
202
+ if self._installed_app is app and self._application.state is ApplicationState.COMPOSED:
195
203
  try:
196
- if self._application.state is ApplicationState.COMPOSED:
197
- await self._application.stop()
204
+ await self._application.stop()
198
205
  finally:
199
- self._clear_container_bridge(app)
206
+ self._clear_application_bridges(app)
200
207
  raise
201
208
  try:
202
209
  await self._application.start()
210
+ except InvalidApplicationStateError:
211
+ # Core 在取得转换 ownership 前拒绝重复/并发进入;不能撤销有效 lifespan 的桥。
212
+ raise
203
213
  except BaseException:
204
- self._clear_container_bridge(app)
214
+ self._clear_application_bridges(app)
205
215
  raise
206
216
 
207
217
  try:
@@ -214,7 +224,7 @@ class FastApiAdapter:
214
224
  await self._application.stop()
215
225
  finally:
216
226
  # Container 的 close 始终属于 Application;这里只撤销 Web 引用。
217
- self._clear_container_bridge(app)
227
+ self._clear_application_bridges(app)
218
228
 
219
229
  def _validate_install_target(self, app: FastAPI) -> None:
220
230
  if not isinstance(app, FastAPI):
@@ -225,6 +235,8 @@ class FastApiAdapter:
225
235
  raise FastApiAdapterError("FastAPI instance already has a Python DDD Framework adapter")
226
236
  if hasattr(app.state, _DISHKA_CONTAINER_BRIDGE):
227
237
  raise FastApiAdapterError("FastAPI instance already has a Dishka container bridge")
238
+ if hasattr(app.state, _APPLICATION_STATE_ATTRIBUTE):
239
+ raise FastApiAdapterError("FastAPI instance already has an Application binding")
228
240
  if app.middleware_stack is not None:
229
241
  raise FastApiAdapterError("FastAPI middleware stack was already built before install")
230
242
  if self._application.state is not ApplicationState.COMPOSED:
@@ -239,6 +251,8 @@ class FastApiAdapter:
239
251
  )
240
252
  if getattr(app.state, _INSTALLATION_MARKER, None) != str(self._application.id):
241
253
  raise FastApiAdapterError("FastAPI installation marker changed before startup")
254
+ if getattr(app.state, _APPLICATION_STATE_ATTRIBUTE, None) is not self._application:
255
+ raise FastApiAdapterError("FastAPI Application binding changed before startup")
242
256
  if app.state.python_ddd_framework_filters != self._filters:
243
257
  raise FastApiAdapterError("HTTP filters changed after adapter installation")
244
258
  if tuple(app.user_middleware) != self._installed_middleware:
@@ -258,9 +272,10 @@ class FastApiAdapter:
258
272
  raise FastApiAdapterError("Dishka container bridge changed before startup")
259
273
 
260
274
  @staticmethod
261
- def _clear_container_bridge(app: FastAPI) -> None:
262
- if hasattr(app.state, _DISHKA_CONTAINER_BRIDGE):
263
- delattr(app.state, _DISHKA_CONTAINER_BRIDGE)
275
+ def _clear_application_bridges(app: FastAPI) -> None:
276
+ for attribute in (_DISHKA_CONTAINER_BRIDGE, _APPLICATION_STATE_ATTRIBUTE):
277
+ if hasattr(app.state, attribute):
278
+ delattr(app.state, attribute)
264
279
 
265
280
  async def _shutdown_realtime(self) -> None:
266
281
  try:
@@ -2,10 +2,13 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- from fastapi import APIRouter
5
+ from fastapi import APIRouter, Request
6
6
  from starlette.responses import JSONResponse
7
7
 
8
8
  from ..application import Application
9
+ from .errors import FastApiAdapterError
10
+
11
+ _APPLICATION_STATE_ATTRIBUTE = "_python_ddd_framework_application"
9
12
 
10
13
  HEALTH_LIVE_PATH = "/health/live"
11
14
  HEALTH_READY_PATH = "/health/ready"
@@ -13,18 +16,24 @@ HEALTH_LIVE_OPERATION_ID = "python_ddd_framework_health_live"
13
16
  HEALTH_READY_OPERATION_ID = "python_ddd_framework_health_ready"
14
17
 
15
18
 
16
- def _create_health_router(application: Application) -> APIRouter:
17
- router = APIRouter(tags=["health"])
19
+ async def live() -> JSONResponse:
20
+ return JSONResponse(status_code=200, content={"status": "live"})
21
+
22
+
23
+ async def ready(request: Request) -> JSONResponse:
24
+ # FastAPI 可长期缓存 callable;应用引用只在当前请求期间从实例状态取得。
25
+ application = getattr(request.app.state, _APPLICATION_STATE_ATTRIBUTE, None)
26
+ if not isinstance(application, Application):
27
+ raise FastApiAdapterError("FastAPI Application binding is unavailable")
28
+ is_ready = application.is_ready
29
+ return JSONResponse(
30
+ status_code=200 if is_ready else 503,
31
+ content={"status": "ready" if is_ready else "not_ready"},
32
+ )
18
33
 
19
- async def live() -> JSONResponse:
20
- return JSONResponse(status_code=200, content={"status": "live"})
21
34
 
22
- async def ready() -> JSONResponse:
23
- is_ready = application.is_ready
24
- return JSONResponse(
25
- status_code=200 if is_ready else 503,
26
- content={"status": "ready" if is_ready else "not_ready"},
27
- )
35
+ def _create_health_router() -> APIRouter:
36
+ router = APIRouter(tags=["health"])
28
37
 
29
38
  router.add_api_route(
30
39
  HEALTH_LIVE_PATH,
@@ -135,7 +135,7 @@ def _build_route_plan(
135
135
  )
136
136
  )
137
137
 
138
- health_router = _create_health_router(application)
138
+ health_router = _create_health_router()
139
139
  health_route_start = len(preview.routes)
140
140
  preview.include_router(health_router)
141
141
  health_candidates = _route_candidates(
@@ -332,6 +332,11 @@ def _validate_revision_graph(
332
332
  # walk_revisions 强制 Alembic 在任何 engine 创建前加载并校验组合 graph/depends_on。
333
333
  script = ScriptDirectory.from_config(config)
334
334
  revisions = tuple(script.walk_revisions())
335
+ empty_model_sources = {
336
+ (item.owner, item.registration.branch_label, item.migration_path)
337
+ for item in models.for_connection(connection_name)
338
+ if not item.registration.metadata.tables
339
+ }
335
340
  for source in migration_sources:
336
341
  if source.extension is not None:
337
342
  _validate_extension_prerequisites(models, connection_name, source, script)
@@ -341,7 +346,18 @@ def _validate_revision_graph(
341
346
  if Path(revision.path).resolve().parent == source.migration_path
342
347
  )
343
348
  if not owned_revisions:
344
- if (owner is not None and source.owner != owner) or allow_empty:
349
+ # 仅全局操作豁免纯 ORM 骨架的缺失 revision;不跳过图或数据库 heads 检查。
350
+ # 同一 owner 可贡献多个来源,判定必须限定当前 connection 与具体分支/目录。
351
+ if (
352
+ (owner is not None and source.owner != owner)
353
+ or allow_empty
354
+ or (
355
+ owner is None
356
+ and source.extension is None
357
+ and (source.owner, source.branch_label, source.migration_path)
358
+ in empty_model_sources
359
+ )
360
+ ):
345
361
  continue
346
362
  raise SqlAlchemyModelRegistrationError(
347
363
  owner=str(source.owner),
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-ddd-framework
3
- Version: 0.7.0
3
+ Version: 0.7.1
4
4
  Summary: Modular DDD application framework with project CLI and bundled templates
5
5
  License-Expression: LicenseRef-Proprietary
6
6
  License-File: LICENSE
@@ -304,6 +304,14 @@ The image uses `python -m host.main` without development reload. Runtime install
304
304
 
305
305
  New `ddd` modules contain the full responsibility directories and necessary composition, with no preset business code or tests. `basic` is unchanged. This affects subsequent generation only: upgrades do not delete or rewrite existing consumers' order code. Keep existing business contracts and data under their application owners. The former order implementation and its existing domain test now live in [the independent example](examples/order_management/README.md); its initializer uses the official CLI, requires a new destination, and is not bundled in the runtime wheel. No CLI parameter or dependency has been added.
306
306
 
307
+ ## HTTP lifecycle and global migration checks (0.7.1)
308
+
309
+ - Health endpoints now read their Application from request-local access to the FastAPI instance state. Adapter rollback and lifespan cleanup release the binding; rejected duplicate or concurrent lifespan entry preserves the active lifespan's binding and readiness. Health URLs, responses and readiness semantics are unchanged.
310
+ - Replace Owner filtering used solely to exclude empty ORM skeletons with one `await SqlAlchemyMigrator(application).check_current_heads()` call. Global checks and upgrades waive missing revisions only for ordinary ORM sources whose metadata and revision history are both empty. Sources with tables, historical revisions, migration-only declarations or extensions retain their checks, including unknown database history.
311
+ - Keep the Host's startup orchestration and cleanup. Run the check while the Application is COMPOSED, before starting it; there is no automatic startup check or new configuration. Explicit module operations and first-revision authoring keep their existing behavior.
312
+
313
+ See the [migration guide](docs/development.md#数据库聚合与迁移) and [verification record](docs/status.md#http-回收与迁移来源判定未发布).
314
+
307
315
  <a id="streaming-and-access-migration-07-unreleased"></a>
308
316
 
309
317
  ## Streaming and access migration (0.7)
@@ -546,7 +554,7 @@ Do not remove durable job payload versions merely because their Python APIs were
546
554
  <details>
547
555
  <summary>For maintainers: publishing a changed version</summary>
548
556
 
549
- Routine code and documentation commits may retain the current version. Pull requests to `main` run build, static checks, and short architecture/typing/release-intent checks with read-only permissions. Pushes to `main` run the full release gates, including locked, lowest-direct, and highest allowed dependency samples, but skip publication unless the version increased across the push; initial creation of `main` publishes its current version. Manual workflow runs validate only. A failed run may be retried, but a later same-version push does not resume publication: changed contents require another version bump. Follow [release operations](https://github.com/componet-architecture/python-ddd-framework/blob/main/docs/development.md#发布操作) for publisher setup and retry boundaries. Dependency samples and current API/type checks do not establish backward compatibility or a cross-version upgrade guarantee.
557
+ Routine code and documentation commits may retain the current version. Pushes to `main` run the full release gates, including locked, lowest-direct, and highest allowed dependency samples, but skip publication unless the version increased across the push; initial creation of `main` publishes its current version. Manual workflow runs validate only. A failed run may be retried, but a later same-version push does not resume publication: changed contents require another version bump. Follow [release operations](https://github.com/componet-architecture/python-ddd-framework/blob/main/docs/development.md#发布操作) for publisher setup and retry boundaries. Dependency samples and current API/type checks do not establish backward compatibility or a cross-version upgrade guarantee.
550
558
 
551
559
  </details>
552
560
 
@@ -220,14 +220,14 @@ python_ddd_framework/fastapi/__init__.py,sha256=H_c5k2XhbaNdjRXXxfnd1ibwApaDtSh5
220
220
  python_ddd_framework/fastapi/access.py,sha256=SWMB_ILM8iItiS0gftX1rNmKy9gO2BCJDaEiWa54ScI,5346
221
221
  python_ddd_framework/fastapi/access_options.py,sha256=iXP7X4JCZ-7TcifgOdFB65RhZdZNVKF7l21AUKD-3ws,383
222
222
  python_ddd_framework/fastapi/action.py,sha256=OnOrnlgS2xEkpMqVX0s_y2uS4MaGR7QdKfS9bJWlx1U,7459
223
- python_ddd_framework/fastapi/adapter.py,sha256=9cDdBX0uLp6kiorv49eOX15sNxJM4U8rqu8GMjOj6Mk,13376
223
+ python_ddd_framework/fastapi/adapter.py,sha256=SgnCqcVmq_DwK2UiI2PmZPA5Bygxi1QBtp31ym7SKAg,14120
224
224
  python_ddd_framework/fastapi/application_services.py,sha256=RixZNC5niVAWzShUqfxFU_THHQlzxTbwfUTdxETU3qM,11909
225
225
  python_ddd_framework/fastapi/background.py,sha256=MZASgFRz1tQkzMo7A33ZA41qS90wpboLrOdL3RHc87w,1616
226
226
  python_ddd_framework/fastapi/contracts.py,sha256=I9EFDcAuv_Xlv1iNxKWIo4krt3-vkfJgV4ngPKWhxPM,11888
227
227
  python_ddd_framework/fastapi/error_handlers.py,sha256=jH8tbW9nGd_znwhZHSDZv3KwN7k59FFi0QZQWpCWLcg,4492
228
228
  python_ddd_framework/fastapi/errors.py,sha256=XPhxj-WyWQ2wj1q1JkalnyVi2UaOKHbFk_-RbMnS5Ok,984
229
229
  python_ddd_framework/fastapi/filters.py,sha256=aQQB4imJa9XaZUsVkIwmkZ9iZH54P_36wulc7RIKuXU,3954
230
- python_ddd_framework/fastapi/health.py,sha256=GjHN8AnUAU5LpeUl9-U7ScnI7deeHJ3u4pm4fOnCMA0,1312
230
+ python_ddd_framework/fastapi/health.py,sha256=Gjoebhx-6fH4go48xmw3dfo9qUKhVHBxcZfbxkB5Ndg,1703
231
231
  python_ddd_framework/fastapi/http.py,sha256=W0jFoqVYOc_rvT7BGa9m1btEThixX7o0ujt_TMxegsg,2725
232
232
  python_ddd_framework/fastapi/http_router.py,sha256=pgfzy7zcUgy8bNOUTIRn7vgeSHLLcVcVbufLute_m_Y,9254
233
233
  python_ddd_framework/fastapi/manual_action.py,sha256=1HMqRttXK1JiN3uuKGmYuss9hRAzussI-dilGhGfns8,5992
@@ -242,7 +242,7 @@ python_ddd_framework/fastapi/realtime/options.py,sha256=wfEPu6q6dmydx5m6h9E_muie
242
242
  python_ddd_framework/fastapi/realtime/runtime.py,sha256=OL0tAOcLDUY_IXlkzXMLzPkAynpa1jExbtliDbmtct8,15197
243
243
  python_ddd_framework/fastapi/request_context.py,sha256=3EWO44HgumKQOFHeyP_dVF3qTRbo1lzbhR1K1qSvxqo,10592
244
244
  python_ddd_framework/fastapi/route_integrity.py,sha256=_tDeeE6XHbsgrA2i1m6Yz_I9NU_JVuYb1E3iMvbwMxE,3174
245
- python_ddd_framework/fastapi/routing.py,sha256=E4rjQPC58SmjfN8_HBQXWgcGMWoc_oZw3XvMG2mny64,16353
245
+ python_ddd_framework/fastapi/routing.py,sha256=dvTBWAJVV8TINtY98vCCsN_NVCZGi5h75XqoDfzfRMQ,16342
246
246
  python_ddd_framework/fastapi/server.py,sha256=o8klk9sUJvBuLRBM8VU5bp1pFJwRHYafrImI5VGK8GQ,7538
247
247
  python_ddd_framework/fastapi/service_endpoints.py,sha256=RpbC-9sEu2JF2JiwsogKnp7bpOhgvgjw6EtAadLzHuM,7549
248
248
  python_ddd_framework/fastapi/service_routes.py,sha256=qOWYcIkOtIdYcMXdDIHan0DtklkRieqtBWXd9BQPuBU,9329
@@ -430,7 +430,7 @@ python_ddd_framework/sqlalchemy/metadata.py,sha256=cuQV6ER_PY-LdmKmpK-riQRY1mpyp
430
430
  python_ddd_framework/sqlalchemy/metadata_builder.py,sha256=FaMs0jPqO4uWuK_ZjIF25xIycy6SK2BfFCpx_UXii6Y,9588
431
431
  python_ddd_framework/sqlalchemy/metadata_fingerprint.py,sha256=LDP0Q9I_RmtJYxa84Pvhw8GLLhlxRwy010qe6FC0MYw,4307
432
432
  python_ddd_framework/sqlalchemy/metadata_ownership.py,sha256=OO2Y-1wbn8qwz35qXdW7nV9HJqf7dc82qFHjCwr8K7I,2750
433
- python_ddd_framework/sqlalchemy/migration.py,sha256=jBOUA6Bg-nY7O-Q5kHb5eqdohm1GrhdMYA5-f3Kp7pA,17569
433
+ python_ddd_framework/sqlalchemy/migration.py,sha256=H8ke8F-4AnjTvRdZfaZD2WJYMhUYDO3kNddtad3cy4k,18305
434
434
  python_ddd_framework/sqlalchemy/migration_operations.py,sha256=y_hH8yc11k8wftVMhTHGGpR6-DuIPftxDeUa_YCEn0U,3770
435
435
  python_ddd_framework/sqlalchemy/migration_options.py,sha256=4_VK8Ei1U0twC3dnpfnzUblM9x2kqhuRPgEoB9ahtQc,2082
436
436
  python_ddd_framework/sqlalchemy/migration_sources.py,sha256=mre0ktTV9ooDXjNf_LI9zNuh1eJWatpwaWIRGd60wsA,1175
@@ -452,9 +452,9 @@ python_ddd_framework/unit_of_work/module.py,sha256=EjsUtCG7w56NmLU_xqBRiwJ65Mw5h
452
452
  python_ddd_framework/unit_of_work/options.py,sha256=AVcEmwwn39qvj8PLQetBeGzdU_eblABGuNLDIQU5tyI,3365
453
453
  python_ddd_framework/validation/__init__.py,sha256=nTN_BgIXMLHeUtMgSj2hpisWc1_sk6UWTapk30oh1us,100
454
454
  python_ddd_framework/validation/models.py,sha256=Ydz5ZYgqYMPZEXi9A3DA6beN5lD9AcMaImnxDf1aRXs,1737
455
- python_ddd_framework-0.7.0.dist-info/licenses/LICENSE,sha256=2sSWLZdtk4EhG2GlwpksXzmyux-G6Qiov0D27lE_bqw,805
456
- python_ddd_framework-0.7.0.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt,sha256=0lCGUd5Pke2Up1Kqn1kRnHcwLlBz60E8xvR7WL4k_kE,1066
457
- python_ddd_framework-0.7.0.dist-info/WHEEL,sha256=cmC5s21ojypbVslldL7IJq3hjZH-tINy4rziKePFsG0,81
458
- python_ddd_framework-0.7.0.dist-info/entry_points.txt,sha256=CMy4XBD8IKGUfHwe0YBmHmSJtjfnA3yyDHqsMZCLsc8,447
459
- python_ddd_framework-0.7.0.dist-info/METADATA,sha256=WZ28DdKm19LsVDQU-bXI3vDRK3WPlOOUrpzw4G_BR9c,64292
460
- python_ddd_framework-0.7.0.dist-info/RECORD,,
455
+ python_ddd_framework-0.7.1.dist-info/licenses/LICENSE,sha256=2sSWLZdtk4EhG2GlwpksXzmyux-G6Qiov0D27lE_bqw,805
456
+ python_ddd_framework-0.7.1.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt,sha256=0lCGUd5Pke2Up1Kqn1kRnHcwLlBz60E8xvR7WL4k_kE,1066
457
+ python_ddd_framework-0.7.1.dist-info/WHEEL,sha256=xTCBFIZIIyRSxyowOv5Wqg_jFU6KsdIffuulBB09Ph8,80
458
+ python_ddd_framework-0.7.1.dist-info/entry_points.txt,sha256=CMy4XBD8IKGUfHwe0YBmHmSJtjfnA3yyDHqsMZCLsc8,447
459
+ python_ddd_framework-0.7.1.dist-info/METADATA,sha256=wuV2ZKpyQAD-DUgVtmUbUsGa-fTUYranSs28gys5IgI,65390
460
+ python_ddd_framework-0.7.1.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: uv 0.12.23
2
+ Generator: uv 0.13.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any