ddd4py 0.1.0__tar.gz

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 (61) hide show
  1. ddd4py-0.1.0/.github/workflows/ci.yml +43 -0
  2. ddd4py-0.1.0/.github/workflows/publish.yml +75 -0
  3. ddd4py-0.1.0/.gitignore +133 -0
  4. ddd4py-0.1.0/LICENSE +21 -0
  5. ddd4py-0.1.0/PKG-INFO +123 -0
  6. ddd4py-0.1.0/README.md +104 -0
  7. ddd4py-0.1.0/Taskfile.yml +38 -0
  8. ddd4py-0.1.0/mise.toml +4 -0
  9. ddd4py-0.1.0/pyproject.toml +128 -0
  10. ddd4py-0.1.0/src/ddd4py/__init__.py +44 -0
  11. ddd4py-0.1.0/src/ddd4py/application/__init__.py +9 -0
  12. ddd4py-0.1.0/src/ddd4py/application/application_service_life_cycle.py +121 -0
  13. ddd4py-0.1.0/src/ddd4py/application/unit_of_work.py +53 -0
  14. ddd4py-0.1.0/src/ddd4py/domain/__init__.py +0 -0
  15. ddd4py-0.1.0/src/ddd4py/domain/model/__init__.py +11 -0
  16. ddd4py-0.1.0/src/ddd4py/domain/model/domain_event.py +90 -0
  17. ddd4py-0.1.0/src/ddd4py/domain/model/domain_registry.py +20 -0
  18. ddd4py-0.1.0/src/ddd4py/domain/model/event_context.py +27 -0
  19. ddd4py-0.1.0/src/ddd4py/event/__init__.py +5 -0
  20. ddd4py-0.1.0/src/ddd4py/event/event_context_provider.py +56 -0
  21. ddd4py-0.1.0/src/ddd4py/event/event_store.py +24 -0
  22. ddd4py-0.1.0/src/ddd4py/event/stored_event.py +74 -0
  23. ddd4py-0.1.0/src/ddd4py/exception/__init__.py +4 -0
  24. ddd4py-0.1.0/src/ddd4py/exception/error_code.py +50 -0
  25. ddd4py-0.1.0/src/ddd4py/exception/system_exception.py +16 -0
  26. ddd4py-0.1.0/src/ddd4py/module.py +81 -0
  27. ddd4py-0.1.0/src/ddd4py/notification/__init__.py +20 -0
  28. ddd4py-0.1.0/src/ddd4py/notification/consumed_notification.py +31 -0
  29. ddd4py-0.1.0/src/ddd4py/notification/consumed_notification_store.py +36 -0
  30. ddd4py-0.1.0/src/ddd4py/notification/notification.py +72 -0
  31. ddd4py-0.1.0/src/ddd4py/notification/notification_publisher.py +9 -0
  32. ddd4py-0.1.0/src/ddd4py/notification/notification_reader.py +33 -0
  33. ddd4py-0.1.0/src/ddd4py/notification/notification_serializer.py +17 -0
  34. ddd4py-0.1.0/src/ddd4py/notification/published_notification_tracker.py +60 -0
  35. ddd4py-0.1.0/src/ddd4py/notification/published_notification_tracker_store.py +17 -0
  36. ddd4py-0.1.0/src/ddd4py/port/__init__.py +0 -0
  37. ddd4py-0.1.0/src/ddd4py/port/adapter/__init__.py +0 -0
  38. ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/__init__.py +5 -0
  39. ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/exchange_listener.py +19 -0
  40. ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/message_publisher.py +13 -0
  41. ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/message_subscriber.py +112 -0
  42. ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/stub/__init__.py +3 -0
  43. ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/stub/message_publisher_stub.py +19 -0
  44. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/__init__.py +0 -0
  45. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/__init__.py +11 -0
  46. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/in_mem_consumed_notification_store.py +27 -0
  47. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/in_mem_event_store.py +31 -0
  48. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/in_mem_published_notification_tracker_store.py +20 -0
  49. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/in_mem_unit_of_work.py +37 -0
  50. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/sqlalchemy/__init__.py +4 -0
  51. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/sqlalchemy/session_preparer.py +28 -0
  52. ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/sqlalchemy/sqlalchemy_unit_of_work.py +110 -0
  53. ddd4py-0.1.0/src/ddd4py/settings.py +40 -0
  54. ddd4py-0.1.0/src/ddd4py/testing/__init__.py +15 -0
  55. ddd4py-0.1.0/src/ddd4py/testing/contracts.py +111 -0
  56. ddd4py-0.1.0/src/ddd4py/testing/di.py +34 -0
  57. ddd4py-0.1.0/test/__init__.py +0 -0
  58. ddd4py-0.1.0/test/test_/343/203/210/343/203/251/343/203/263/343/202/266/343/202/257/343/202/267/343/203/247/343/203/263/345/242/203/347/225/214.py +155 -0
  59. ddd4py-0.1.0/test/test_/343/203/235/343/203/274/343/203/210/351/201/251/345/220/210/343/203/206/343/202/271/343/203/210.py +46 -0
  60. ddd4py-0.1.0/test/test_/345/256/237/350/241/214/346/226/207/350/204/210/343/201/256/345/210/273/345/215/260.py +98 -0
  61. ddd4py-0.1.0/uv.lock +536 -0
@@ -0,0 +1,43 @@
1
+ name: Python CI
2
+
3
+ on:
4
+ push:
5
+ branches: [ main ]
6
+ pull_request:
7
+ branches: [ main ]
8
+ workflow_dispatch:
9
+
10
+ permissions: {}
11
+
12
+ jobs:
13
+ validation:
14
+ name: 検査
15
+ runs-on: ubuntu-latest
16
+ timeout-minutes: 10
17
+ permissions:
18
+ contents: read
19
+ steps:
20
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
21
+ with:
22
+ persist-credentials: false
23
+
24
+ - name: uv のセットアップ
25
+ # setup-uv は major/minor タグを publish していないため commit hash でピンする
26
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
27
+ with:
28
+ python-version: "3.14"
29
+ enable-cache: true
30
+
31
+ - name: 依存の同期
32
+ # --locked で uv.lock と pyproject.toml のズレも検出する。
33
+ # --all-extras は Taskfile の init と同じ経路 (mypy が sqlalchemy アダプタも検査する)。
34
+ run: uv sync --locked --all-extras
35
+
36
+ - name: Ruff リント
37
+ run: uv run ruff check .
38
+
39
+ - name: 型チェック
40
+ run: uv run mypy
41
+
42
+ - name: テスト
43
+ run: uv run pytest
@@ -0,0 +1,75 @@
1
+ name: Publish to PyPI
2
+
3
+ on:
4
+ release:
5
+ types: [published]
6
+
7
+ # 既定は無権限。必要な job にだけ最小権限を与える。
8
+ permissions: {}
9
+
10
+ concurrency:
11
+ group: publish-${{ github.ref }}
12
+ cancel-in-progress: false
13
+
14
+ jobs:
15
+ build:
16
+ name: 配布物のビルド
17
+ runs-on: ubuntu-latest
18
+ timeout-minutes: 10
19
+ permissions:
20
+ contents: read
21
+ steps:
22
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
23
+ with:
24
+ persist-credentials: false
25
+
26
+ - name: uv のセットアップ
27
+ # setup-uv は major/minor タグを publish していないため commit hash でピンする
28
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
29
+ with:
30
+ python-version: "3.14"
31
+ # リリース成果物のビルドではキャッシュを使わない。
32
+ # PR 経由で汚染されたキャッシュが配布物に混入する経路を断つ (zizmor: cache-poisoning)。
33
+ enable-cache: false
34
+
35
+ - name: タグと pyproject.toml の version が一致することを検証
36
+ env:
37
+ TAG: ${{ github.event.release.tag_name }}
38
+ run: |
39
+ VERSION="$(python3 -c 'import tomllib, pathlib; print(tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"])')"
40
+ if [ "${TAG#v}" != "$VERSION" ]; then
41
+ echo "::error::タグ ${TAG} と pyproject.toml の version ${VERSION} が一致しません"
42
+ exit 1
43
+ fi
44
+
45
+ - name: sdist と wheel をビルド
46
+ run: uv build --out-dir dist
47
+
48
+ - name: 配布物を artifact として保存
49
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
50
+ with:
51
+ name: python-package-distributions
52
+ path: dist/
53
+
54
+ publish-to-pypi:
55
+ name: PyPI へ公開
56
+ needs: [ build ]
57
+ runs-on: ubuntu-latest
58
+ timeout-minutes: 10
59
+ environment:
60
+ name: pypi
61
+ url: https://pypi.org/p/ddd4py
62
+ permissions:
63
+ # OIDC トークンの発行に必須。workflow 単位ではなく job 単位で付与する (公式の強い推奨)
64
+ id-token: write
65
+ steps:
66
+ - name: 配布物を取得
67
+ uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
68
+ with:
69
+ name: python-package-distributions
70
+ path: dist/
71
+
72
+ - name: PyPI へアップロード
73
+ # Trusted Publishing (OIDC) のため認証情報の指定は一切不要。
74
+ # v1.11.0 以降、Trusted Publishing 時は PEP 740 attestation が自動生成・添付される。
75
+ uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
@@ -0,0 +1,133 @@
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ pip-wheel-metadata/
24
+ share/python-wheels/
25
+ *.egg-info/
26
+ .installed.cfg
27
+ *.egg
28
+ MANIFEST
29
+
30
+ # PyInstaller
31
+ # Usually these files are written by a python script from a template
32
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
33
+ *.manifest
34
+ *.spec
35
+
36
+ # Installer logs
37
+ pip-log.txt
38
+ pip-delete-this-directory.txt
39
+
40
+ # Unit test / coverage reports
41
+ htmlcov/
42
+ .tox/
43
+ .nox/
44
+ .coverage
45
+ .coverage.*
46
+ .cache
47
+ nosetests.xml
48
+ coverage.xml
49
+ *.cover
50
+ *.py,cover
51
+ .hypothesis/
52
+ .pytest_cache/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ target/
76
+
77
+ # Jupyter Notebook
78
+ .ipynb_checkpoints
79
+
80
+ # IPython
81
+ profile_default/
82
+ ipython_config.py
83
+
84
+ # pyenv
85
+ .python-version
86
+
87
+ # pipenv
88
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
89
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
90
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
91
+ # install all needed dependencies.
92
+ #Pipfile.lock
93
+
94
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow
95
+ __pypackages__/
96
+
97
+ # Celery stuff
98
+ celerybeat-schedule
99
+ celerybeat.pid
100
+
101
+ # SageMath parsed files
102
+ *.sage.py
103
+
104
+ # Environments
105
+ .env
106
+ .venv
107
+ env/
108
+ venv/
109
+ ENV/
110
+ env.bak/
111
+ venv.bak/
112
+
113
+ # Spyder project settings
114
+ .spyderproject
115
+ .spyproject
116
+
117
+ # Rope project settings
118
+ .ropeproject
119
+
120
+ # mkdocs documentation
121
+ /site
122
+
123
+ # mypy
124
+ .mypy_cache/
125
+ .dmypy.json
126
+ dmypy.json
127
+
128
+ # Pyre type checker
129
+ .pyre/
130
+
131
+ # JetBrains IDE (IntelliJ IDEA / PyCharm)
132
+ # 各開発者のローカル設定なので git 管理しない
133
+ .idea/
ddd4py-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 taiyo tamura
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
ddd4py-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,123 @@
1
+ Metadata-Version: 2.5
2
+ Name: ddd4py
3
+ Version: 0.1.0
4
+ Summary: モジュラモノリス + DDD のカーネル。集約・ドメインイベント・UnitOfWork・トランザクショナル outbox/inbox を業務語彙ゼロで提供する
5
+ Project-URL: Homepage, https://github.com/theindiehacker/ddd4py
6
+ Project-URL: Repository, https://github.com/theindiehacker/ddd4py
7
+ Author-email: taiyo tamura <gtaiyou24@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Keywords: ddd,hexagonal-architecture,modular-monolith,outbox,unit-of-work
11
+ Requires-Python: >=3.14
12
+ Requires-Dist: di4injector==0.0.2
13
+ Requires-Dist: injector==0.22.0
14
+ Requires-Dist: pydantic-settings==2.14.2
15
+ Requires-Dist: pytz==2026.2
16
+ Provides-Extra: sqlalchemy
17
+ Requires-Dist: sqlalchemy==2.0.51; extra == 'sqlalchemy'
18
+ Description-Content-Type: text/markdown
19
+
20
+ # 🧱 DDD for python
21
+
22
+ モジュラモノリス + ドメイン駆動設計のカーネル。**業務語彙をひとつも持たない**ことを設計制約にしている。
23
+
24
+ アーキテクチャ契約の検査は [clean-architecture](https://github.com/theindiehacker/clean-architecture) が担う。
25
+ このリポジトリは「import できるカーネル」だけを配る。
26
+
27
+ ## 入っているもの
28
+
29
+ | 層 | 提供するもの |
30
+ |:--|:--|
31
+ | 合成 | `AppModule` / `CompositeModule` — モジュールの宣言点と合成ルート |
32
+ | 集約 | `DomainEvent` / `DomainEventPublisher` / `DomainEventSubscriber` / `DomainRegistry` |
33
+ | ユースケース | `UnitOfWork` / `ApplicationServiceLifeCycle` / `@transactional` |
34
+ | outbox | `StoredEvent` / `EventStore` / `EventContextProvider` |
35
+ | inbox | `ConsumedNotification` / `ConsumedNotificationStore` / `MessageSubscriber` |
36
+ | 発行 | `Notification` / `NotificationPublisher` / `PublishedNotificationTracker` |
37
+ | アダプタ | `InMem*`(テスト用) / `SQLAlchemyUnitOfWork`(extras: `sqlalchemy`) |
38
+ | 適合テスト | `ddd4py.testing.verify_*` — 自分のアダプタ実装が契約を満たすか検証する |
39
+
40
+ ## 導入
41
+
42
+ ```bash
43
+ uv add ddd4py
44
+ uv add "ddd4py[sqlalchemy]" # SQLAlchemy アダプタも使う場合
45
+ ```
46
+
47
+ ## トランザクションと配送の保証
48
+
49
+ `@transactional` の内側で publish されたドメインイベントは、集約の更新と**同一トランザクション**で
50
+ outbox(`StoredEvent`)に追記される。ネストした境界は最外に join し、内側で失敗すれば最外まで巻き戻る
51
+ (部分 commit を許さない)。
52
+
53
+ 受信側は `MessageSubscriber._dispatch` が listener ごとにトランザクションを開始し、
54
+ consumed marker の INSERT(claim-before-process)と listener の副作用を単一トランザクションで commit する
55
+ (Idempotent Consumer / transactional inbox)。at-least-once の重複は marker の unique 制約が先勝ち 1 つに絞る。
56
+
57
+ ## 実行文脈というただ 1 つの拡張点
58
+
59
+ カーネルは「いま誰のリクエストを処理しているか」を知らない。知っているのは、outbox にそれを**刻印する**
60
+ ことと、受信時にそれを**確立する**ことだけ。
61
+
62
+ ```python
63
+ from ddd4py import EventContext, EventContextProvider
64
+
65
+ class TenantContextProvider(EventContextProvider):
66
+ def current(self) -> EventContext:
67
+ tenant = CurrentTenant.get()
68
+ return EventContext(partition_key=tenant.id, payload=tenant.to_dict())
69
+
70
+ @contextmanager
71
+ def bind(self, context: EventContext) -> Iterator[None]:
72
+ with CurrentTenant.of(context.payload).bind():
73
+ yield
74
+ ```
75
+
76
+ 単一テナントなら `NullEventContextProvider` のままでよい。
77
+
78
+ ## 案件ごとの差し替え
79
+
80
+ `CompositeModule` は並べた順に DI を登録し、**後ろに置いたモジュールが前の束縛を差し替える**。
81
+ 案件固有の実装は継承や上書きではなく、後ろにモジュールを足すことで注入する。
82
+
83
+ ```python
84
+ CompositeModule([Core(), Authority(), Tenant(), AcmeOverrides()])
85
+ ```
86
+
87
+ ## 適合テスト
88
+
89
+ 自分のアダプタ実装が契約を満たすかを、**利用側の CI で**検証する。
90
+
91
+ ```python
92
+ from ddd4py.testing import verify_consumed_notification_store
93
+
94
+ def test_postgresql_consumed_notification_store(store):
95
+ verify_consumed_notification_store(store)
96
+ ```
97
+
98
+ ## 開発
99
+
100
+ ```bash
101
+ task init # 依存インストール
102
+ task test # テスト
103
+ task style:check # ruff / mypy
104
+ task style:check:arch # clean-architecture による自分自身への契約検査
105
+ ```
106
+
107
+ ## リリース
108
+
109
+ PyPI への公開は GitHub Release をトリガーに、[Trusted Publishing (OIDC)](https://docs.pypi.org/trusted-publishers/) で自動実行される。
110
+ API トークンはリポジトリに置かない。
111
+
112
+ ```bash
113
+ # 1. pyproject.toml の version を上げて main へマージする
114
+ # 2. その version と同じタグで Release を作る (v プレフィックス付き)
115
+ gh release create v0.1.0 --generate-notes
116
+ ```
117
+
118
+ タグと `pyproject.toml` の version が食い違うとワークフローは公開前に落ちる。
119
+ `__version__` は `pyproject.toml` から読むため、バージョンを書き換える箇所は `pyproject.toml` の 1 行だけ。
120
+
121
+ ## ライセンス
122
+
123
+ MIT
ddd4py-0.1.0/README.md ADDED
@@ -0,0 +1,104 @@
1
+ # 🧱 DDD for python
2
+
3
+ モジュラモノリス + ドメイン駆動設計のカーネル。**業務語彙をひとつも持たない**ことを設計制約にしている。
4
+
5
+ アーキテクチャ契約の検査は [clean-architecture](https://github.com/theindiehacker/clean-architecture) が担う。
6
+ このリポジトリは「import できるカーネル」だけを配る。
7
+
8
+ ## 入っているもの
9
+
10
+ | 層 | 提供するもの |
11
+ |:--|:--|
12
+ | 合成 | `AppModule` / `CompositeModule` — モジュールの宣言点と合成ルート |
13
+ | 集約 | `DomainEvent` / `DomainEventPublisher` / `DomainEventSubscriber` / `DomainRegistry` |
14
+ | ユースケース | `UnitOfWork` / `ApplicationServiceLifeCycle` / `@transactional` |
15
+ | outbox | `StoredEvent` / `EventStore` / `EventContextProvider` |
16
+ | inbox | `ConsumedNotification` / `ConsumedNotificationStore` / `MessageSubscriber` |
17
+ | 発行 | `Notification` / `NotificationPublisher` / `PublishedNotificationTracker` |
18
+ | アダプタ | `InMem*`(テスト用) / `SQLAlchemyUnitOfWork`(extras: `sqlalchemy`) |
19
+ | 適合テスト | `ddd4py.testing.verify_*` — 自分のアダプタ実装が契約を満たすか検証する |
20
+
21
+ ## 導入
22
+
23
+ ```bash
24
+ uv add ddd4py
25
+ uv add "ddd4py[sqlalchemy]" # SQLAlchemy アダプタも使う場合
26
+ ```
27
+
28
+ ## トランザクションと配送の保証
29
+
30
+ `@transactional` の内側で publish されたドメインイベントは、集約の更新と**同一トランザクション**で
31
+ outbox(`StoredEvent`)に追記される。ネストした境界は最外に join し、内側で失敗すれば最外まで巻き戻る
32
+ (部分 commit を許さない)。
33
+
34
+ 受信側は `MessageSubscriber._dispatch` が listener ごとにトランザクションを開始し、
35
+ consumed marker の INSERT(claim-before-process)と listener の副作用を単一トランザクションで commit する
36
+ (Idempotent Consumer / transactional inbox)。at-least-once の重複は marker の unique 制約が先勝ち 1 つに絞る。
37
+
38
+ ## 実行文脈というただ 1 つの拡張点
39
+
40
+ カーネルは「いま誰のリクエストを処理しているか」を知らない。知っているのは、outbox にそれを**刻印する**
41
+ ことと、受信時にそれを**確立する**ことだけ。
42
+
43
+ ```python
44
+ from ddd4py import EventContext, EventContextProvider
45
+
46
+ class TenantContextProvider(EventContextProvider):
47
+ def current(self) -> EventContext:
48
+ tenant = CurrentTenant.get()
49
+ return EventContext(partition_key=tenant.id, payload=tenant.to_dict())
50
+
51
+ @contextmanager
52
+ def bind(self, context: EventContext) -> Iterator[None]:
53
+ with CurrentTenant.of(context.payload).bind():
54
+ yield
55
+ ```
56
+
57
+ 単一テナントなら `NullEventContextProvider` のままでよい。
58
+
59
+ ## 案件ごとの差し替え
60
+
61
+ `CompositeModule` は並べた順に DI を登録し、**後ろに置いたモジュールが前の束縛を差し替える**。
62
+ 案件固有の実装は継承や上書きではなく、後ろにモジュールを足すことで注入する。
63
+
64
+ ```python
65
+ CompositeModule([Core(), Authority(), Tenant(), AcmeOverrides()])
66
+ ```
67
+
68
+ ## 適合テスト
69
+
70
+ 自分のアダプタ実装が契約を満たすかを、**利用側の CI で**検証する。
71
+
72
+ ```python
73
+ from ddd4py.testing import verify_consumed_notification_store
74
+
75
+ def test_postgresql_consumed_notification_store(store):
76
+ verify_consumed_notification_store(store)
77
+ ```
78
+
79
+ ## 開発
80
+
81
+ ```bash
82
+ task init # 依存インストール
83
+ task test # テスト
84
+ task style:check # ruff / mypy
85
+ task style:check:arch # clean-architecture による自分自身への契約検査
86
+ ```
87
+
88
+ ## リリース
89
+
90
+ PyPI への公開は GitHub Release をトリガーに、[Trusted Publishing (OIDC)](https://docs.pypi.org/trusted-publishers/) で自動実行される。
91
+ API トークンはリポジトリに置かない。
92
+
93
+ ```bash
94
+ # 1. pyproject.toml の version を上げて main へマージする
95
+ # 2. その version と同じタグで Release を作る (v プレフィックス付き)
96
+ gh release create v0.1.0 --generate-notes
97
+ ```
98
+
99
+ タグと `pyproject.toml` の version が食い違うとワークフローは公開前に落ちる。
100
+ `__version__` は `pyproject.toml` から読むため、バージョンを書き換える箇所は `pyproject.toml` の 1 行だけ。
101
+
102
+ ## ライセンス
103
+
104
+ MIT
@@ -0,0 +1,38 @@
1
+ version: '3'
2
+
3
+ tasks:
4
+ init:
5
+ desc: 開発環境のセットアップ
6
+ cmds:
7
+ - uv sync --all-extras
8
+ - echo "✅ done"
9
+
10
+ test:
11
+ desc: テスト実行
12
+ cmd: uv run pytest {{.CLI_ARGS}}
13
+
14
+ style:check:
15
+ desc: スタイル / 型の検査
16
+ deps: [style:check:ruff, style:check:mypy]
17
+ cmds:
18
+ - echo "✅ ddd4py"
19
+
20
+ style:check:ruff:
21
+ desc: Ruff リント
22
+ cmd: uv run ruff check .
23
+
24
+ style:check:mypy:
25
+ desc: 型チェック
26
+ cmd: uv run mypy
27
+
28
+ style:check:arch:
29
+ desc: 自分自身へのアーキテクチャ検査(clean-architecture のドッグフーディング)
30
+ cmd: uvx --from git+https://github.com/theindiehacker/clean-architecture cleanarch check
31
+
32
+ style:fix:
33
+ desc: スタイル自動修正
34
+ cmd: uv run ruff check --fix .
35
+
36
+ build:
37
+ desc: 配布物のビルド
38
+ cmd: uv build --out-dir dist
ddd4py-0.1.0/mise.toml ADDED
@@ -0,0 +1,4 @@
1
+ [tools]
2
+ python = "3.14"
3
+ "github:astral-sh/uv" = "latest"
4
+ task = "latest"
@@ -0,0 +1,128 @@
1
+ [project]
2
+ name = "ddd4py"
3
+ version = "0.1.0"
4
+ description = "モジュラモノリス + DDD のカーネル。集約・ドメインイベント・UnitOfWork・トランザクショナル outbox/inbox を業務語彙ゼロで提供する"
5
+ readme = "README.md"
6
+ requires-python = ">=3.14"
7
+ license = "MIT"
8
+ license-files = ["LICENSE"]
9
+ authors = [{ name = "taiyo tamura", email = "gtaiyou24@gmail.com" }]
10
+ keywords = ["ddd", "modular-monolith", "hexagonal-architecture", "unit-of-work", "outbox"]
11
+ dependencies = [
12
+ "di4injector==0.0.2",
13
+ "injector==0.22.0",
14
+ "pydantic-settings==2.14.2",
15
+ "pytz==2026.2",
16
+ ]
17
+
18
+ [project.optional-dependencies]
19
+ # L1 アダプタ。カーネル本体 (L0) は永続化技術に依存しない。
20
+ sqlalchemy = ["sqlalchemy==2.0.51"]
21
+
22
+ [project.urls]
23
+ Homepage = "https://github.com/theindiehacker/ddd4py"
24
+ Repository = "https://github.com/theindiehacker/ddd4py"
25
+
26
+ [build-system]
27
+ requires = ["hatchling"]
28
+ build-backend = "hatchling.build"
29
+
30
+ [tool.hatch.build.targets.wheel]
31
+ packages = ["src/ddd4py"]
32
+
33
+ [dependency-groups]
34
+ dev = [
35
+ "mypy==2.1.0",
36
+ "pytest==9.1.1",
37
+ "pytest-mock==3.15.1",
38
+ "ruff==0.15.21",
39
+ "sqlalchemy==2.0.51",
40
+ ]
41
+
42
+ # ============ 🏛 clean-architecture ============
43
+ # 自分のカーネルを自分の検査ツールで守る (ドッグフーディング)。
44
+ # カーネルは単一パッケージなので modules は 1 つだが、契約 (layers / 入力アダプタ) は
45
+ # 利用側プロジェクトに配るものと完全に同一のコードが生成する。
46
+ # 実行には clean-architecture (cleanarch) が要る:
47
+ # uvx --from git+https://github.com/theindiehacker/clean-architecture cleanarch check
48
+ [tool.cleanarch]
49
+ src = "src"
50
+ modules = ["ddd4py"]
51
+ layer_ignores = ["module", "settings", "exception", "event", "notification", "testing"]
52
+
53
+ # ============ 🪄 Ruff ============
54
+ # カーネルの規約は利用側プロジェクトに配る規約と揃える (自分が守れない規約は配れない)。
55
+ [tool.ruff]
56
+ # src レイアウト。ddd4py を第一パーティとして isort に認識させる。
57
+ src = ["src", "."]
58
+ target-version = "py314"
59
+ line-length = 120
60
+ indent-width = 4
61
+ extend-exclude = ["__init__.py"]
62
+
63
+ [tool.ruff.lint]
64
+ select = ["ALL"]
65
+ ignore = [
66
+ "B008",
67
+ "ERA001",
68
+ "FIX002",
69
+ "N818",
70
+ "PLR0913",
71
+ "RUF012",
72
+ "TRY003",
73
+ "FBT001",
74
+ "FBT002",
75
+ "PLC0415",
76
+ "PLW0108",
77
+ "A003",
78
+ "D",
79
+ "TD",
80
+ "ARG",
81
+ "EM",
82
+ ]
83
+
84
+ [tool.ruff.lint.per-file-ignores]
85
+ # 適合テストキットは利用側の pytest から呼ばれる公開 API。assert は仕様そのもの。
86
+ "src/ddd4py/testing/**/*.py" = ["S101", "PLR2004"]
87
+ # テストは日本語で仕様を書く (テスト名がそのまま仕様書になる)。
88
+ "test/**/*.py" = ["S101", "SLF001", "PLR2004", "INP001", "N801", "N802", "N803", "N806", "N999", "PLC2401", "RUF001", "RUF002", "RUF003",
89
+ # テストは fixture / tmp_path を実行時に受け取るため型限定 import へ移せない
90
+ "TC001", "TC002", "TC003",
91
+ # 期限判定は「今日」が基準。テストでも同じ基準を使う
92
+ "DTZ011"]
93
+
94
+ [tool.ruff.lint.pycodestyle]
95
+ max-line-length = 160
96
+
97
+ [tool.ruff.lint.flake8-annotations]
98
+ mypy-init-return = true
99
+
100
+ [tool.ruff.lint.flake8-tidy-imports]
101
+ ban-relative-imports = "all"
102
+
103
+ [tool.ruff.lint.isort.sections]
104
+ sections = ["FUTURE", "STDLIB", "THIRDPARTY", "FIRSTPARTY", "LOCALFOLDER"]
105
+
106
+ [tool.ruff.lint.flake8-type-checking]
107
+ runtime-evaluated-decorators = ["injector.inject"]
108
+ runtime-evaluated-base-classes = ["pydantic.BaseModel", "pydantic_settings.BaseSettings"]
109
+
110
+ # ============ ✔️ mypy ============
111
+ [tool.mypy]
112
+ python_version = 3.14
113
+ files = ["src", "test"]
114
+ mypy_path = ["src"]
115
+ color_output = true
116
+ show_column_numbers = true
117
+ check_untyped_defs = true
118
+ disallow_untyped_defs = true
119
+ no_implicit_optional = true
120
+ warn_redundant_casts = true
121
+ ignore_missing_imports = true
122
+ strict_optional = false
123
+ explicit_package_bases = true
124
+
125
+ # ============ 🧪 pytest ============
126
+ [tool.pytest.ini_options]
127
+ testpaths = ["test"]
128
+ pythonpath = ["src"]