python-ddd-framework 0.3.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.
Files changed (355) hide show
  1. python_ddd_framework/__init__.py +449 -0
  2. python_ddd_framework/application/__init__.py +6 -0
  3. python_ddd_framework/application/build_spec.py +37 -0
  4. python_ddd_framework/application/builder.py +260 -0
  5. python_ddd_framework/application/composition.py +219 -0
  6. python_ddd_framework/application/runtime.py +549 -0
  7. python_ddd_framework/application/runtime_hooks.py +327 -0
  8. python_ddd_framework/application/state.py +216 -0
  9. python_ddd_framework/application_services/__init__.py +54 -0
  10. python_ddd_framework/application_services/catalog.py +500 -0
  11. python_ddd_framework/application_services/contracts.py +190 -0
  12. python_ddd_framework/application_services/dispatcher.py +423 -0
  13. python_ddd_framework/application_services/errors.py +89 -0
  14. python_ddd_framework/application_services/execution.py +199 -0
  15. python_ddd_framework/application_services/interceptors.py +44 -0
  16. python_ddd_framework/application_services/invocation.py +431 -0
  17. python_ddd_framework/application_services/policies.py +46 -0
  18. python_ddd_framework/application_services/seeding.py +72 -0
  19. python_ddd_framework/application_services/signature.py +49 -0
  20. python_ddd_framework/application_services/validation.py +84 -0
  21. python_ddd_framework/auditing/__init__.py +12 -0
  22. python_ddd_framework/auditing/contracts.py +39 -0
  23. python_ddd_framework/auditing/control.py +48 -0
  24. python_ddd_framework/auditing/sqlalchemy/__init__.py +6 -0
  25. python_ddd_framework/auditing/sqlalchemy/migrations/0001_auditing.py +45 -0
  26. python_ddd_framework/auditing/sqlalchemy/migrations/__init__.py +1 -0
  27. python_ddd_framework/auditing/sqlalchemy/models.py +31 -0
  28. python_ddd_framework/auditing/sqlalchemy/module.py +32 -0
  29. python_ddd_framework/auditing/sqlalchemy/store.py +63 -0
  30. python_ddd_framework/authorization/__init__.py +38 -0
  31. python_ddd_framework/authorization/catalog.py +63 -0
  32. python_ddd_framework/authorization/contracts.py +241 -0
  33. python_ddd_framework/authorization/definition_discovery.py +81 -0
  34. python_ddd_framework/authorization/definitions.py +43 -0
  35. python_ddd_framework/authorization/errors.py +40 -0
  36. python_ddd_framework/background_execution/__init__.py +17 -0
  37. python_ddd_framework/background_execution/application.py +41 -0
  38. python_ddd_framework/background_execution/child.py +123 -0
  39. python_ddd_framework/background_execution/contracts.py +48 -0
  40. python_ddd_framework/background_execution/lifecycle.py +22 -0
  41. python_ddd_framework/background_execution/local.py +127 -0
  42. python_ddd_framework/background_execution/locks.py +33 -0
  43. python_ddd_framework/background_execution/management.py +17 -0
  44. python_ddd_framework/background_execution/module.py +10 -0
  45. python_ddd_framework/background_execution/permissions.py +14 -0
  46. python_ddd_framework/background_execution/processes.py +331 -0
  47. python_ddd_framework/background_jobs/__init__.py +31 -0
  48. python_ddd_framework/background_jobs/catalog.py +340 -0
  49. python_ddd_framework/background_jobs/contracts.py +208 -0
  50. python_ddd_framework/background_jobs/declaration.py +67 -0
  51. python_ddd_framework/background_jobs/errors.py +28 -0
  52. python_ddd_framework/background_jobs/execution.py +203 -0
  53. python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
  54. python_ddd_framework/background_jobs/pgqueuer/__init__.py +6 -0
  55. python_ddd_framework/background_jobs/pgqueuer/enqueue.py +86 -0
  56. python_ddd_framework/background_jobs/pgqueuer/migrations/0001_pgqueuer_1_3_2.py +95 -0
  57. python_ddd_framework/background_jobs/pgqueuer/migrations/__init__.py +1 -0
  58. python_ddd_framework/background_jobs/pgqueuer/module.py +106 -0
  59. python_ddd_framework/background_jobs/pgqueuer/options.py +82 -0
  60. python_ddd_framework/background_jobs/pgqueuer/runtime.py +479 -0
  61. python_ddd_framework/background_jobs/pgqueuer/sql/pgqueuer_1_3_2_install.sql +118 -0
  62. python_ddd_framework/background_jobs/pgqueuer/supervision.py +201 -0
  63. python_ddd_framework/background_workers/__init__.py +22 -0
  64. python_ddd_framework/background_workers/catalog.py +118 -0
  65. python_ddd_framework/background_workers/contracts.py +152 -0
  66. python_ddd_framework/background_workers/errors.py +20 -0
  67. python_ddd_framework/background_workers/execution.py +133 -0
  68. python_ddd_framework/background_workers/runtime.py +219 -0
  69. python_ddd_framework/caching/__init__.py +11 -0
  70. python_ddd_framework/caching/catalog.py +73 -0
  71. python_ddd_framework/caching/contracts.py +73 -0
  72. python_ddd_framework/caching/errors.py +20 -0
  73. python_ddd_framework/cli/__init__.py +105 -0
  74. python_ddd_framework/cli/development.py +85 -0
  75. python_ddd_framework/cli/errors.py +5 -0
  76. python_ddd_framework/cli/inspection.py +96 -0
  77. python_ddd_framework/cli/project.py +69 -0
  78. python_ddd_framework/cli/runtime.py +56 -0
  79. python_ddd_framework/configuration/__init__.py +21 -0
  80. python_ddd_framework/configuration/composition.py +65 -0
  81. python_ddd_framework/configuration/contracts.py +68 -0
  82. python_ddd_framework/configuration/dotenv_source.py +51 -0
  83. python_ddd_framework/configuration/environment_source.py +38 -0
  84. python_ddd_framework/configuration/immutability.py +124 -0
  85. python_ddd_framework/configuration/input_shape.py +58 -0
  86. python_ddd_framework/configuration/merge.py +103 -0
  87. python_ddd_framework/configuration/root.py +215 -0
  88. python_ddd_framework/configuration/sources.py +519 -0
  89. python_ddd_framework/configuration/values.py +427 -0
  90. python_ddd_framework/configuration/yaml_source.py +51 -0
  91. python_ddd_framework/developer_kit/__init__.py +1 -0
  92. python_ddd_framework/developer_kit/generation.py +182 -0
  93. python_ddd_framework/developer_kit/project_metadata.py +45 -0
  94. python_ddd_framework/developer_kit/source.py +53 -0
  95. python_ddd_framework/developer_kit/templates/module/cookiecutter.json +1 -0
  96. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/README.md +70 -0
  97. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/__init__.py.jinja +1 -0
  98. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/__init__.py.jinja +1 -0
  99. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/cache.py.jinja +10 -0
  100. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/events.py.jinja +24 -0
  101. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/integration.py.jinja +41 -0
  102. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/module.py.jinja +20 -0
  103. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/options.py.jinja +7 -0
  104. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/orders.py.jinja +83 -0
  105. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application/tasks.py.jinja +84 -0
  106. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/__init__.py.jinja +1 -0
  107. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/module.py.jinja +10 -0
  108. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/application_contracts/orders.py.jinja +35 -0
  109. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/__init__.py.jinja +1 -0
  110. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/module.py.jinja +10 -0
  111. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/orders.py.jinja +56 -0
  112. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/repository.py.jinja +14 -0
  113. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/seeding.py.jinja +19 -0
  114. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain/settings.py.jinja +16 -0
  115. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/__init__.py.jinja +1 -0
  116. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/definitions.py.jinja +24 -0
  117. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/module.py.jinja +7 -0
  118. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/domain_shared/permissions.py.jinja +15 -0
  119. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/__init__.py.jinja +1 -0
  120. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/files.py.jinja +73 -0
  121. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/module.py.jinja +25 -0
  122. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/http_api/realtime.py.jinja +36 -0
  123. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/__init__.py.jinja +1 -0
  124. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/migrations/__init__.py.jinja +1 -0
  125. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/__init__.py.jinja +7 -0
  126. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/models/orders.py.jinja +25 -0
  127. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/module.py.jinja +26 -0
  128. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/__init__.py.jinja +1 -0
  129. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/sqlalchemy/repositories/orders.py.jinja +49 -0
  130. python_ddd_framework/developer_kit/templates/module/{{cookiecutter.module_name}}/tests/test_domain.py.jinja +18 -0
  131. python_ddd_framework/developer_kit/templates/project/cookiecutter.json +6 -0
  132. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.dockerignore +9 -0
  133. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.gitignore +6 -0
  134. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/.python-version +1 -0
  135. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/AGENTS.md +38 -0
  136. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/Dockerfile +22 -0
  137. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/README.md +130 -0
  138. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/app.development.yaml.jinja +30 -0
  139. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.dev.yaml.jinja +24 -0
  140. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/compose.production.yaml.jinja +20 -0
  141. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/architecture.md +84 -0
  142. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/docs/development.md +185 -0
  143. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/pyproject.toml.jinja +28 -0
  144. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/__init__.py.jinja +1 -0
  145. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/main.py.jinja +47 -0
  146. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/src/host/module.py.jinja +37 -0
  147. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/conftest.py.jinja +8 -0
  148. python_ddd_framework/developer_kit/templates/project/{{cookiecutter.project_name}}/tests/host/test_http.py.jinja +57 -0
  149. python_ddd_framework/developer_kit/wiring.py +162 -0
  150. python_ddd_framework/diagnostics/__init__.py +23 -0
  151. python_ddd_framework/diagnostics/journal.py +174 -0
  152. python_ddd_framework/diagnostics/model.py +83 -0
  153. python_ddd_framework/diagnostics/source.py +25 -0
  154. python_ddd_framework/distributed_lock/__init__.py +6 -0
  155. python_ddd_framework/distributed_lock/contracts.py +20 -0
  156. python_ddd_framework/distributed_lock/options.py +19 -0
  157. python_ddd_framework/domain/__init__.py +13 -0
  158. python_ddd_framework/domain/aggregates.py +64 -0
  159. python_ddd_framework/domain/entities.py +33 -0
  160. python_ddd_framework/domain/errors.py +12 -0
  161. python_ddd_framework/domain/value_objects.py +31 -0
  162. python_ddd_framework/errors/__init__.py +75 -0
  163. python_ddd_framework/errors/base.py +13 -0
  164. python_ddd_framework/errors/business.py +53 -0
  165. python_ddd_framework/errors/configuration.py +119 -0
  166. python_ddd_framework/errors/diagnostics.py +12 -0
  167. python_ddd_framework/errors/lifecycle.py +164 -0
  168. python_ddd_framework/errors/modularity.py +100 -0
  169. python_ddd_framework/errors/services.py +100 -0
  170. python_ddd_framework/events/__init__.py +25 -0
  171. python_ddd_framework/events/aggregate.py +39 -0
  172. python_ddd_framework/events/catalog.py +136 -0
  173. python_ddd_framework/events/contracts.py +39 -0
  174. python_ddd_framework/events/contribution.py +128 -0
  175. python_ddd_framework/events/discovery.py +105 -0
  176. python_ddd_framework/events/errors.py +48 -0
  177. python_ddd_framework/events/runtime.py +192 -0
  178. python_ddd_framework/fastapi/__init__.py +49 -0
  179. python_ddd_framework/fastapi/action.py +175 -0
  180. python_ddd_framework/fastapi/adapter.py +391 -0
  181. python_ddd_framework/fastapi/application_services.py +654 -0
  182. python_ddd_framework/fastapi/background.py +42 -0
  183. python_ddd_framework/fastapi/contracts.py +275 -0
  184. python_ddd_framework/fastapi/errors.py +32 -0
  185. python_ddd_framework/fastapi/filters.py +111 -0
  186. python_ddd_framework/fastapi/health.py +44 -0
  187. python_ddd_framework/fastapi/http.py +72 -0
  188. python_ddd_framework/fastapi/http_router.py +216 -0
  189. python_ddd_framework/fastapi/manual_action.py +105 -0
  190. python_ddd_framework/fastapi/middleware.py +93 -0
  191. python_ddd_framework/fastapi/parameters.py +97 -0
  192. python_ddd_framework/fastapi/realtime/__init__.py +13 -0
  193. python_ddd_framework/fastapi/realtime/authentication.py +181 -0
  194. python_ddd_framework/fastapi/realtime/connection.py +166 -0
  195. python_ddd_framework/fastapi/realtime/module.py +41 -0
  196. python_ddd_framework/fastapi/realtime/options.py +50 -0
  197. python_ddd_framework/fastapi/realtime/runtime.py +423 -0
  198. python_ddd_framework/fastapi/request_context.py +247 -0
  199. python_ddd_framework/fastapi/route_integrity.py +88 -0
  200. python_ddd_framework/fastapi/routing.py +424 -0
  201. python_ddd_framework/fastapi/server.py +182 -0
  202. python_ddd_framework/fastapi/settings.py +40 -0
  203. python_ddd_framework/fastapi/tracing.py +62 -0
  204. python_ddd_framework/fastapi/transfer.py +128 -0
  205. python_ddd_framework/fastapi/upload_limits.py +63 -0
  206. python_ddd_framework/fastapi/uploads.py +94 -0
  207. python_ddd_framework/hosted_services/__init__.py +29 -0
  208. python_ddd_framework/hosted_services/bridge.py +420 -0
  209. python_ddd_framework/hosted_services/catalog.py +265 -0
  210. python_ddd_framework/hosted_services/contracts.py +55 -0
  211. python_ddd_framework/hosted_services/errors.py +53 -0
  212. python_ddd_framework/hosted_services/options.py +17 -0
  213. python_ddd_framework/hosted_services/runtime.py +267 -0
  214. python_ddd_framework/hosted_services/state.py +40 -0
  215. python_ddd_framework/hosting/__init__.py +4 -0
  216. python_ddd_framework/hosting/instance.py +40 -0
  217. python_ddd_framework/identity/__init__.py +73 -0
  218. python_ddd_framework/identity/application.py +287 -0
  219. python_ddd_framework/identity/contracts.py +283 -0
  220. python_ddd_framework/identity/errors.py +28 -0
  221. python_ddd_framework/identity/http_api.py +90 -0
  222. python_ddd_framework/identity/module.py +49 -0
  223. python_ddd_framework/identity/passwords.py +34 -0
  224. python_ddd_framework/identity/permissions.py +14 -0
  225. python_ddd_framework/identity/services.py +108 -0
  226. python_ddd_framework/identity/sqlalchemy/__init__.py +5 -0
  227. python_ddd_framework/identity/sqlalchemy/migrations/0001_identity.py +143 -0
  228. python_ddd_framework/identity/sqlalchemy/migrations/0002_physical_delete.py +42 -0
  229. python_ddd_framework/identity/sqlalchemy/migrations/__init__.py +1 -0
  230. python_ddd_framework/identity/sqlalchemy/models.py +85 -0
  231. python_ddd_framework/identity/sqlalchemy/module.py +62 -0
  232. python_ddd_framework/identity/sqlalchemy/stores.py +526 -0
  233. python_ddd_framework/identity/tokens.py +106 -0
  234. python_ddd_framework/invocation/__init__.py +3 -0
  235. python_ddd_framework/invocation/callables.py +171 -0
  236. python_ddd_framework/invocation/contracts.py +33 -0
  237. python_ddd_framework/invocation/entries.py +54 -0
  238. python_ddd_framework/invocation/function_runtime.py +48 -0
  239. python_ddd_framework/invocation/interception.py +193 -0
  240. python_ddd_framework/lifecycle/__init__.py +25 -0
  241. python_ddd_framework/lifecycle/composition.py +35 -0
  242. python_ddd_framework/lifecycle/context.py +31 -0
  243. python_ddd_framework/lifecycle/runtime.py +43 -0
  244. python_ddd_framework/lifecycle/state.py +20 -0
  245. python_ddd_framework/modularity/__init__.py +15 -0
  246. python_ddd_framework/modularity/contracts.py +91 -0
  247. python_ddd_framework/modularity/discovery.py +157 -0
  248. python_ddd_framework/modularity/graph.py +334 -0
  249. python_ddd_framework/modularity/registry.py +100 -0
  250. python_ddd_framework/modularity/selection.py +16 -0
  251. python_ddd_framework/notifications/__init__.py +19 -0
  252. python_ddd_framework/notifications/catalog.py +46 -0
  253. python_ddd_framework/notifications/contracts.py +107 -0
  254. python_ddd_framework/observability/__init__.py +5 -0
  255. python_ddd_framework/observability/context.py +43 -0
  256. python_ddd_framework/observability/export.py +73 -0
  257. python_ddd_framework/observability/formatting.py +89 -0
  258. python_ddd_framework/observability/logging.py +118 -0
  259. python_ddd_framework/observability/options.py +45 -0
  260. python_ddd_framework/observability/tracing.py +80 -0
  261. python_ddd_framework/options/__init__.py +19 -0
  262. python_ddd_framework/options/aliases.py +331 -0
  263. python_ddd_framework/options/contribution.py +62 -0
  264. python_ddd_framework/options/immutability.py +48 -0
  265. python_ddd_framework/options/input_keys.py +214 -0
  266. python_ddd_framework/options/issues.py +335 -0
  267. python_ddd_framework/options/models.py +114 -0
  268. python_ddd_framework/options/registry.py +280 -0
  269. python_ddd_framework/options/schema.py +488 -0
  270. python_ddd_framework/options/validation.py +120 -0
  271. python_ddd_framework/py.typed +0 -0
  272. python_ddd_framework/realtime/__init__.py +12 -0
  273. python_ddd_framework/realtime/contracts.py +28 -0
  274. python_ddd_framework/realtime/diagnostics.py +37 -0
  275. python_ddd_framework/realtime/messages.py +113 -0
  276. python_ddd_framework/redis/__init__.py +6 -0
  277. python_ddd_framework/redis/distributed_lock.py +212 -0
  278. python_ddd_framework/redis/lease_lock.py +36 -0
  279. python_ddd_framework/redis/module.py +50 -0
  280. python_ddd_framework/redis/notification_runtime.py +182 -0
  281. python_ddd_framework/redis/notifications.py +25 -0
  282. python_ddd_framework/redis/options.py +35 -0
  283. python_ddd_framework/redis/runtime.py +116 -0
  284. python_ddd_framework/services/__init__.py +41 -0
  285. python_ddd_framework/services/application_bindings.py +221 -0
  286. python_ddd_framework/services/arbitration.py +381 -0
  287. python_ddd_framework/services/binding.py +102 -0
  288. python_ddd_framework/services/contribution.py +438 -0
  289. python_ddd_framework/services/convention.py +301 -0
  290. python_ddd_framework/services/convention_contracts.py +101 -0
  291. python_ddd_framework/services/exposure.py +23 -0
  292. python_ddd_framework/services/fixed_lifetime.py +52 -0
  293. python_ddd_framework/services/framework_provider.py +178 -0
  294. python_ddd_framework/services/native_graph.py +201 -0
  295. python_ddd_framework/services/provider.py +204 -0
  296. python_ddd_framework/services/registration.py +101 -0
  297. python_ddd_framework/services/repository.py +38 -0
  298. python_ddd_framework/services/runtime.py +302 -0
  299. python_ddd_framework/settings/__init__.py +30 -0
  300. python_ddd_framework/settings/application.py +95 -0
  301. python_ddd_framework/settings/binding.py +33 -0
  302. python_ddd_framework/settings/catalog.py +121 -0
  303. python_ddd_framework/settings/changes.py +14 -0
  304. python_ddd_framework/settings/contracts.py +115 -0
  305. python_ddd_framework/settings/definition_discovery.py +71 -0
  306. python_ddd_framework/settings/definitions.py +41 -0
  307. python_ddd_framework/settings/errors.py +22 -0
  308. python_ddd_framework/settings/handlers.py +43 -0
  309. python_ddd_framework/settings/management.py +57 -0
  310. python_ddd_framework/settings/manager.py +89 -0
  311. python_ddd_framework/settings/module.py +10 -0
  312. python_ddd_framework/settings/notifications.py +39 -0
  313. python_ddd_framework/settings/permissions.py +14 -0
  314. python_ddd_framework/settings/provider.py +74 -0
  315. python_ddd_framework/settings/refresh.py +140 -0
  316. python_ddd_framework/settings/refresh_module.py +45 -0
  317. python_ddd_framework/settings/sqlalchemy/__init__.py +5 -0
  318. python_ddd_framework/settings/sqlalchemy/migrations/0001_settings.py +31 -0
  319. python_ddd_framework/settings/sqlalchemy/migrations/0002_physical_delete.py +32 -0
  320. python_ddd_framework/settings/sqlalchemy/migrations/0003_version_tokens.py +39 -0
  321. python_ddd_framework/settings/sqlalchemy/migrations/__init__.py +1 -0
  322. python_ddd_framework/settings/sqlalchemy/models.py +36 -0
  323. python_ddd_framework/settings/sqlalchemy/module.py +36 -0
  324. python_ddd_framework/settings/sqlalchemy/store.py +86 -0
  325. python_ddd_framework/settings/store.py +16 -0
  326. python_ddd_framework/settings/values.py +39 -0
  327. python_ddd_framework/sqlalchemy/__init__.py +54 -0
  328. python_ddd_framework/sqlalchemy/alembic_runtime/__init__.py +1 -0
  329. python_ddd_framework/sqlalchemy/alembic_runtime/env.py +33 -0
  330. python_ddd_framework/sqlalchemy/alembic_runtime/script.py.mako +14 -0
  331. python_ddd_framework/sqlalchemy/auditing.py +48 -0
  332. python_ddd_framework/sqlalchemy/errors.py +85 -0
  333. python_ddd_framework/sqlalchemy/metadata.py +576 -0
  334. python_ddd_framework/sqlalchemy/migration.py +378 -0
  335. python_ddd_framework/sqlalchemy/migration_options.py +56 -0
  336. python_ddd_framework/sqlalchemy/module.py +51 -0
  337. python_ddd_framework/sqlalchemy/module_migration.py +151 -0
  338. python_ddd_framework/sqlalchemy/options.py +66 -0
  339. python_ddd_framework/sqlalchemy/repository.py +33 -0
  340. python_ddd_framework/sqlalchemy/runtime.py +85 -0
  341. python_ddd_framework/sqlalchemy/session_provider.py +45 -0
  342. python_ddd_framework/sqlalchemy/unit_of_work.py +97 -0
  343. python_ddd_framework/testing/__init__.py +5 -0
  344. python_ddd_framework/testing/runtime.py +112 -0
  345. python_ddd_framework/unit_of_work/__init__.py +14 -0
  346. python_ddd_framework/unit_of_work/contracts.py +223 -0
  347. python_ddd_framework/unit_of_work/errors.py +27 -0
  348. python_ddd_framework/unit_of_work/manager.py +210 -0
  349. python_ddd_framework/unit_of_work/options.py +92 -0
  350. python_ddd_framework-0.3.1.dist-info/METADATA +379 -0
  351. python_ddd_framework-0.3.1.dist-info/RECORD +355 -0
  352. python_ddd_framework-0.3.1.dist-info/WHEEL +4 -0
  353. python_ddd_framework-0.3.1.dist-info/entry_points.txt +9 -0
  354. python_ddd_framework-0.3.1.dist-info/licenses/LICENSE +7 -0
  355. python_ddd_framework-0.3.1.dist-info/licenses/src/python_ddd_framework/background_jobs/pgqueuer/UPSTREAM_LICENSE.txt +21 -0
@@ -0,0 +1,275 @@
1
+ """Module-local Router metadata 与不可变 HTTP diagnostics contract。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Awaitable, Callable
6
+ from dataclasses import dataclass
7
+ from typing import TypeVar
8
+
9
+ from fastapi import APIRouter
10
+
11
+ from ..application_services import ApplicationService, ApplicationServiceContract
12
+ from ..diagnostics import SourceLocation
13
+ from ..modularity import AppModule, ModuleKey
14
+ from .errors import FastApiAdapterError
15
+
16
+ _ROUTER_METADATA_ATTRIBUTE = "__python_ddd_framework_fastapi_routers__"
17
+ _APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE = (
18
+ "__python_ddd_framework_fastapi_application_service_exposure__"
19
+ )
20
+ _ModuleType = TypeVar("_ModuleType", bound=type[AppModule])
21
+ _HTTP_VERBS = frozenset({"GET", "POST", "PUT", "DELETE", "PATCH"})
22
+ _ROOT_PATH_SEGMENT_CHARACTERS = frozenset("abcdefghijklmnopqrstuvwxyz0123456789-")
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class RouterRegistration:
27
+ owner: ModuleKey
28
+ source: SourceLocation
29
+ path: str
30
+ methods: tuple[str, ...]
31
+ operation_id: str
32
+ order: int
33
+ http_sources: tuple[tuple[str, str], ...] = ()
34
+
35
+
36
+ @dataclass(frozen=True, slots=True)
37
+ class WebSocketRouteRegistration:
38
+ owner: ModuleKey
39
+ source: SourceLocation
40
+ path: str
41
+ order: int
42
+
43
+
44
+ class RouterCatalog:
45
+ """Application-local route 诊断目录;HTTP 与 WebSocket 保持独立视图。"""
46
+
47
+ __slots__ = ("_registrations", "_websocket_registrations")
48
+
49
+ def __init__(
50
+ self,
51
+ registrations: tuple[RouterRegistration, ...],
52
+ websocket_registrations: tuple[WebSocketRouteRegistration, ...] = (),
53
+ ) -> None:
54
+ self._registrations = registrations
55
+ self._websocket_registrations = websocket_registrations
56
+
57
+ @property
58
+ def registrations(self) -> tuple[RouterRegistration, ...]:
59
+ return self._registrations
60
+
61
+ def for_owner(self, owner: ModuleKey) -> tuple[RouterRegistration, ...]:
62
+ return tuple(item for item in self._registrations if item.owner == owner)
63
+
64
+ @property
65
+ def websocket_registrations(self) -> tuple[WebSocketRouteRegistration, ...]:
66
+ return self._websocket_registrations
67
+
68
+ def websockets_for_owner(
69
+ self,
70
+ owner: ModuleKey,
71
+ ) -> tuple[WebSocketRouteRegistration, ...]:
72
+ return tuple(item for item in self._websocket_registrations if item.owner == owner)
73
+
74
+
75
+ @dataclass(frozen=True, slots=True)
76
+ class ApplicationServiceRouteOverride:
77
+ """用 Protocol identity 覆盖单个 convention route,或排除整个 service。"""
78
+
79
+ service: type[object]
80
+ method: Callable[..., Awaitable[object]] | None = None
81
+ enabled: bool = True
82
+ path: str | None = None
83
+ verb: str | None = None
84
+ status_code: int | None = None
85
+ operation_id: str | None = None
86
+ include_in_schema: bool | None = None
87
+
88
+ def __post_init__(self) -> None:
89
+ _require_application_service_contract(self.service, role="route override service")
90
+ if self.method is not None and not callable(self.method):
91
+ raise FastApiAdapterError("route override method must be a Protocol method")
92
+ if not isinstance(self.enabled, bool):
93
+ raise FastApiAdapterError("route override enabled must be bool")
94
+ if self.path is not None and (
95
+ not isinstance(self.path, str)
96
+ or not self.path.startswith("/")
97
+ or self.path == "/"
98
+ or self.path.endswith("/")
99
+ or "//" in self.path
100
+ ):
101
+ raise FastApiAdapterError(
102
+ "route override path must be a non-root absolute path without a trailing slash"
103
+ )
104
+ _validate_schema_visibility(self.include_in_schema)
105
+ normalized_verb = self.verb.upper() if isinstance(self.verb, str) else self.verb
106
+ if normalized_verb is not None and normalized_verb not in _HTTP_VERBS:
107
+ raise FastApiAdapterError(
108
+ "route override verb must be GET, POST, PUT, DELETE, or PATCH"
109
+ )
110
+ if normalized_verb is not None:
111
+ object.__setattr__(self, "verb", normalized_verb)
112
+ if self.status_code is not None and (
113
+ not isinstance(self.status_code, int)
114
+ or isinstance(self.status_code, bool)
115
+ or not 100 <= self.status_code <= 599
116
+ ):
117
+ raise FastApiAdapterError("route override status_code must be between 100 and 599")
118
+ if self.operation_id is not None and (
119
+ not isinstance(self.operation_id, str)
120
+ or not self.operation_id
121
+ or self.operation_id != self.operation_id.strip()
122
+ ):
123
+ raise FastApiAdapterError(
124
+ "route override operation_id must be a non-empty trimmed string"
125
+ )
126
+ route_fields = (
127
+ self.path,
128
+ self.verb,
129
+ self.status_code,
130
+ self.operation_id,
131
+ self.include_in_schema,
132
+ )
133
+ if self.method is None and (self.enabled or any(item is not None for item in route_fields)):
134
+ raise FastApiAdapterError("service-level route override only supports enabled=False")
135
+ if not self.enabled and any(item is not None for item in route_fields):
136
+ raise FastApiAdapterError(
137
+ "disabled route override cannot also set path, verb, status_code, or operation_id"
138
+ )
139
+
140
+
141
+ @dataclass(frozen=True, slots=True)
142
+ class _ApplicationServiceExposure:
143
+ target_modules: tuple[type[AppModule], ...]
144
+ root_path: str
145
+ overrides: tuple[ApplicationServiceRouteOverride, ...]
146
+
147
+
148
+ def contributes_routers(*routers: APIRouter) -> Callable[[_ModuleType], _ModuleType]:
149
+ """把 immutable router tuple 附着到当前 Module class,不建立 global registry。"""
150
+
151
+ if not routers:
152
+ raise FastApiAdapterError("router contribution must contain at least one APIRouter")
153
+ if any(not isinstance(router, APIRouter) for router in routers):
154
+ raise FastApiAdapterError("router contribution items must be FastAPI APIRouter instances")
155
+
156
+ def decorate(module_type: _ModuleType) -> _ModuleType:
157
+ if not isinstance(module_type, type) or not issubclass(module_type, AppModule):
158
+ raise FastApiAdapterError("contributes_routers target must be an AppModule class")
159
+ # vars() 强制 class-local;父类 metadata 不会被子 Module 隐式重复贡献。
160
+ if _ROUTER_METADATA_ATTRIBUTE in vars(module_type):
161
+ raise FastApiAdapterError(
162
+ f"{module_type.__module__}:{module_type.__qualname__} already declares routers"
163
+ )
164
+ setattr(module_type, _ROUTER_METADATA_ATTRIBUTE, routers)
165
+ return module_type
166
+
167
+ return decorate
168
+
169
+
170
+ def exposes_application_services(
171
+ *target_modules: type[AppModule],
172
+ root_path: str = "",
173
+ overrides: tuple[ApplicationServiceRouteOverride, ...] = (),
174
+ ) -> Callable[[_ModuleType], _ModuleType]:
175
+ """让 HTTP Module 显式暴露其直接依赖 Module 拥有的 Application Services。"""
176
+
177
+ if not target_modules:
178
+ raise FastApiAdapterError(
179
+ "application service exposure must declare at least one target Module"
180
+ )
181
+ if any(
182
+ not isinstance(module_type, type) or not issubclass(module_type, AppModule)
183
+ for module_type in target_modules
184
+ ):
185
+ raise FastApiAdapterError("application service exposure targets must be AppModule classes")
186
+ if len(set(target_modules)) != len(target_modules):
187
+ raise FastApiAdapterError("application service exposure contains duplicate target Modules")
188
+ normalized_root_path = _validate_root_path(root_path)
189
+ if not isinstance(overrides, tuple) or any(
190
+ not isinstance(item, ApplicationServiceRouteOverride) for item in overrides
191
+ ):
192
+ raise FastApiAdapterError(
193
+ "application service exposure overrides must be an immutable tuple"
194
+ )
195
+ exposure = _ApplicationServiceExposure(
196
+ target_modules=target_modules,
197
+ root_path=normalized_root_path,
198
+ overrides=overrides,
199
+ )
200
+
201
+ def decorate(module_type: _ModuleType) -> _ModuleType:
202
+ if not isinstance(module_type, type) or not issubclass(module_type, AppModule):
203
+ raise FastApiAdapterError(
204
+ "exposes_application_services target must be an AppModule class"
205
+ )
206
+ if _APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE in vars(module_type):
207
+ raise FastApiAdapterError(
208
+ f"{module_type.__module__}:{module_type.__qualname__} already declares "
209
+ "application service exposure"
210
+ )
211
+ setattr(module_type, _APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE, exposure)
212
+ return module_type
213
+
214
+ return decorate
215
+
216
+
217
+ def _routers_for(module_type: type[AppModule]) -> tuple[APIRouter, ...]:
218
+ raw_metadata = vars(module_type).get(_ROUTER_METADATA_ATTRIBUTE, ())
219
+ if not isinstance(raw_metadata, tuple) or any(
220
+ not isinstance(router, APIRouter) for router in raw_metadata
221
+ ):
222
+ raise FastApiAdapterError(
223
+ f"{module_type.__module__}:{module_type.__qualname__} router metadata "
224
+ "must remain an immutable tuple of APIRouter instances"
225
+ )
226
+ return raw_metadata
227
+
228
+
229
+ def _application_service_exposure_for(
230
+ module_type: type[AppModule],
231
+ ) -> _ApplicationServiceExposure | None:
232
+ metadata = vars(module_type).get(_APPLICATION_SERVICE_EXPOSURE_ATTRIBUTE)
233
+ if metadata is None:
234
+ return None
235
+ if not isinstance(metadata, _ApplicationServiceExposure):
236
+ raise FastApiAdapterError(
237
+ f"{module_type.__module__}:{module_type.__qualname__} application service "
238
+ "exposure metadata has an invalid runtime shape"
239
+ )
240
+ return metadata
241
+
242
+
243
+ def _validate_root_path(root_path: object) -> str:
244
+ if root_path == "":
245
+ return ""
246
+ if not isinstance(root_path, str) or root_path != root_path.strip("/") or "//" in root_path:
247
+ raise FastApiAdapterError("application service exposure root_path must be a relative path")
248
+ for segment in root_path.split("/"):
249
+ if (
250
+ not segment
251
+ or segment.startswith("-")
252
+ or segment.endswith("-")
253
+ or any(character not in _ROOT_PATH_SEGMENT_CHARACTERS for character in segment)
254
+ ):
255
+ raise FastApiAdapterError(
256
+ "application service exposure root_path must use lowercase kebab-case segments"
257
+ )
258
+ return root_path
259
+
260
+
261
+ def _require_application_service_contract(candidate: object, *, role: str) -> None:
262
+ if isinstance(candidate, type) and issubclass(candidate, ApplicationService):
263
+ return
264
+ if (
265
+ not isinstance(candidate, type)
266
+ or not bool(getattr(candidate, "_is_protocol", False))
267
+ or ApplicationServiceContract not in candidate.__mro__
268
+ or candidate is ApplicationServiceContract
269
+ ):
270
+ raise FastApiAdapterError(f"{role} must be an ApplicationServiceContract Protocol")
271
+
272
+
273
+ def _validate_schema_visibility(value: bool | None) -> None:
274
+ if value is not None and not isinstance(value, bool):
275
+ raise FastApiAdapterError("include_in_schema must be bool or None")
@@ -0,0 +1,32 @@
1
+ """FastAPI adapter 安装与 route consistency 的结构化失败。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ..diagnostics import SourceLocation
6
+ from ..errors import FrameworkError
7
+
8
+
9
+ class FastApiAdapterError(FrameworkError):
10
+ """Adapter 无法在不破坏 Application/HTTP 不变量时继续。"""
11
+
12
+ def __init__(self, reason: str) -> None:
13
+ self.reason = reason
14
+ super().__init__(f"FastAPI adapter rejected installation or startup: {reason}")
15
+
16
+
17
+ class RouteConflictError(FastApiAdapterError):
18
+ """两个 HTTP/OpenAPI 声明竞争同一稳定 identity。"""
19
+
20
+ def __init__(
21
+ self,
22
+ *,
23
+ conflict_kind: str,
24
+ value: str,
25
+ sources: tuple[SourceLocation, SourceLocation],
26
+ ) -> None:
27
+ self.conflict_kind = conflict_kind
28
+ self.value = value
29
+ self.sources = sources
30
+ super().__init__(
31
+ f"duplicate {conflict_kind} {value!r} from {sources[0].owner} and {sources[1].owner}"
32
+ )
@@ -0,0 +1,111 @@
1
+ """HTTP 已绑定参数与原始结果边界;不参与进程内服务调用。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import inspect
7
+ from collections.abc import Awaitable, Callable, Sequence
8
+ from dataclasses import dataclass
9
+ from typing import TypeVar
10
+
11
+ from dishka import AsyncContainer, Scope
12
+ from starlette.requests import Request
13
+
14
+ from ..application_services.errors import (
15
+ ApplicationInvocationRejectedError,
16
+ CrossTaskInvocationError,
17
+ )
18
+ from ..invocation.callables import _dependency_key
19
+ from .errors import FastApiAdapterError
20
+
21
+ _T = TypeVar("_T")
22
+ _FILTERS = "__python_ddd_framework_service_filters__"
23
+
24
+
25
+ @dataclass(frozen=True, slots=True)
26
+ class ServiceFilter:
27
+ filter_type: type[object]
28
+ order: int = 0
29
+
30
+ def __post_init__(self) -> None:
31
+ if not isinstance(self.filter_type, type) or not isinstance(self.order, int):
32
+ raise TypeError("service_filter requires a type and integer order")
33
+
34
+
35
+ def service_filter(filter_type: type[object], *, order: int = 0) -> Callable[[_T], _T]:
36
+ registration = ServiceFilter(filter_type, order)
37
+
38
+ def decorate(target: _T) -> _T:
39
+ # Python 由下向上应用装饰器;公开声明顺序仍按源码从上向下。
40
+ setattr(target, _FILTERS, (registration, *vars(target).get(_FILTERS, ())))
41
+ return target
42
+
43
+ return decorate
44
+
45
+
46
+ def _filters_for(*targets: object) -> tuple[ServiceFilter, ...]:
47
+ return tuple(item for target in targets for item in vars(target).get(_FILTERS, ()))
48
+
49
+
50
+ def _ordered_filters(*levels: Sequence[ServiceFilter]) -> tuple[ServiceFilter, ...]:
51
+ return tuple(
52
+ item[3]
53
+ for item in sorted(
54
+ (registration.order, level, index, registration)
55
+ for level, registrations in enumerate(levels)
56
+ for index, registration in enumerate(registrations)
57
+ )
58
+ )
59
+
60
+
61
+ @dataclass(frozen=True, slots=True)
62
+ class ActionFilterContext:
63
+ request: Request
64
+ endpoint: object
65
+ arguments: dict[str, object]
66
+
67
+
68
+ class _FilterContinuation:
69
+ def __init__(self, callback: Callable[[], Awaitable[object]]) -> None:
70
+ self._callback = callback
71
+ self._task = asyncio.current_task()
72
+ self._consumed = False
73
+ self.active = True
74
+
75
+ async def __call__(self) -> object:
76
+ if asyncio.current_task() is not self._task:
77
+ raise CrossTaskInvocationError
78
+ if self._consumed or not self.active:
79
+ raise ApplicationInvocationRejectedError(reason="filter continuation is unavailable")
80
+ self._consumed = True
81
+ return await self._callback()
82
+
83
+
84
+ async def _run_filters(
85
+ registrations: tuple[ServiceFilter, ...],
86
+ container: AsyncContainer,
87
+ context: ActionFilterContext,
88
+ target: Callable[[], Awaitable[object]],
89
+ ) -> object:
90
+ async def invoke(index: int) -> object:
91
+ if index == len(registrations):
92
+ return await target()
93
+ instance = await container.get(registrations[index].filter_type)
94
+ continuation = _FilterContinuation(lambda: invoke(index + 1))
95
+ try:
96
+ return await instance.invoke(context, continuation) # type: ignore[attr-defined]
97
+ finally:
98
+ continuation.active = False
99
+
100
+ return await invoke(0)
101
+
102
+
103
+ def _validate_filters(registrations: Sequence[ServiceFilter], container: AsyncContainer) -> None:
104
+ for registration in registrations:
105
+ if not isinstance(registration, ServiceFilter):
106
+ raise FastApiAdapterError("HTTP filters must be ServiceFilter declarations")
107
+ parameter = inspect.Parameter("filter", inspect.Parameter.KEYWORD_ONLY)
108
+ if _dependency_key(parameter, registration.filter_type, container, Scope.ACTION) is None:
109
+ raise FastApiAdapterError("HTTP filter type is not registered")
110
+ if not inspect.iscoroutinefunction(getattr(registration.filter_type, "invoke", None)):
111
+ raise FastApiAdapterError("HTTP filter requires async invoke(context, next_action)")
@@ -0,0 +1,44 @@
1
+ """Application lifecycle 导出的固定 HTTP health contract。"""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fastapi import APIRouter
6
+ from starlette.responses import JSONResponse
7
+
8
+ from ..application import Application
9
+
10
+ HEALTH_LIVE_PATH = "/health/live"
11
+ HEALTH_READY_PATH = "/health/ready"
12
+ HEALTH_LIVE_OPERATION_ID = "python_ddd_framework_health_live"
13
+ HEALTH_READY_OPERATION_ID = "python_ddd_framework_health_ready"
14
+
15
+
16
+ def _create_health_router(application: Application) -> APIRouter:
17
+ router = APIRouter(tags=["health"])
18
+
19
+ async def live() -> JSONResponse:
20
+ return JSONResponse(status_code=200, content={"status": "live"})
21
+
22
+ async def ready() -> JSONResponse:
23
+ is_ready = application.readiness.ready
24
+ return JSONResponse(
25
+ status_code=200 if is_ready else 503,
26
+ content={"status": "ready" if is_ready else "not_ready"},
27
+ )
28
+
29
+ router.add_api_route(
30
+ HEALTH_LIVE_PATH,
31
+ live,
32
+ methods=["GET"],
33
+ operation_id=HEALTH_LIVE_OPERATION_ID,
34
+ summary="Liveness",
35
+ )
36
+ router.add_api_route(
37
+ HEALTH_READY_PATH,
38
+ ready,
39
+ methods=["GET"],
40
+ operation_id=HEALTH_READY_OPERATION_ID,
41
+ summary="Readiness",
42
+ responses={503: {"description": "Application is not ready"}},
43
+ )
44
+ return router
@@ -0,0 +1,72 @@
1
+ """实现方法的 HTTP projection 声明;原生路由选项由 FastAPI 校验。"""
2
+
3
+ from collections.abc import Callable, Mapping
4
+ from functools import partial
5
+ from types import MappingProxyType
6
+ from typing import Any, TypeVar, cast
7
+
8
+ from .contracts import _validate_root_path
9
+
10
+ _T = TypeVar("_T")
11
+ _HTTP_ATTRIBUTE = "__python_ddd_framework_http__"
12
+ _REMOTE_ATTRIBUTE = "__python_ddd_framework_remote_service__"
13
+ _SERVICE_PATH_ATTRIBUTE = "__python_ddd_framework_service_path__"
14
+
15
+
16
+ def _declare(verb: str, path: str | None = None, **options: Any) -> Callable[[_T], _T]:
17
+ if path is not None and (not isinstance(path, str) or not path.startswith("/")):
18
+ raise ValueError("http path must be absolute")
19
+ declaration = MappingProxyType(
20
+ {**options, "verb": verb, **({"path": path} if path is not None else {})}
21
+ )
22
+
23
+ def decorate(target: _T) -> _T:
24
+ if not callable(target) or _HTTP_ATTRIBUTE in vars(target):
25
+ raise TypeError("http declaration requires a method declared once")
26
+ setattr(target, _HTTP_ATTRIBUTE, declaration)
27
+ return target
28
+
29
+ return decorate
30
+
31
+
32
+ def _metadata(method: object) -> Mapping[str, Any]:
33
+ return cast(Mapping[str, Any], vars(method).get(_HTTP_ATTRIBUTE, MappingProxyType({})))
34
+
35
+
36
+ def remote_service(
37
+ *, is_enabled: bool = True, is_metadata_enabled: bool = True, path: str | None = None
38
+ ) -> Callable[[_T], _T]:
39
+ if not isinstance(is_enabled, bool) or not isinstance(is_metadata_enabled, bool):
40
+ raise TypeError("remote_service switches must be bool")
41
+ if path is not None:
42
+ _validate_root_path(path)
43
+ if not path or "/" in path:
44
+ raise ValueError("remote_service path must be one non-empty service segment")
45
+
46
+ def decorate(target: _T) -> _T:
47
+ if path is not None and not isinstance(target, type):
48
+ raise TypeError("remote_service path requires an implementation class")
49
+ setattr(target, _REMOTE_ATTRIBUTE, (is_enabled, is_metadata_enabled))
50
+ if path is not None:
51
+ setattr(target, _SERVICE_PATH_ATTRIBUTE, path)
52
+ return target
53
+
54
+ return decorate
55
+
56
+
57
+ def _remote(implementation: type[object], method: object) -> tuple[bool, bool]:
58
+ for target in (method, *implementation.__mro__):
59
+ if _REMOTE_ATTRIBUTE in vars(target):
60
+ return cast(tuple[bool, bool], vars(target)[_REMOTE_ATTRIBUTE])
61
+ return True, True
62
+
63
+
64
+ def _service_segment(implementation: type[object]) -> str | None:
65
+ return cast(str | None, getattr(implementation, _SERVICE_PATH_ATTRIBUTE, None))
66
+
67
+
68
+ get = partial(_declare, "GET")
69
+ post = partial(_declare, "POST")
70
+ put = partial(_declare, "PUT")
71
+ patch = partial(_declare, "PATCH")
72
+ delete = partial(_declare, "DELETE")