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.
- ddd4py-0.1.0/.github/workflows/ci.yml +43 -0
- ddd4py-0.1.0/.github/workflows/publish.yml +75 -0
- ddd4py-0.1.0/.gitignore +133 -0
- ddd4py-0.1.0/LICENSE +21 -0
- ddd4py-0.1.0/PKG-INFO +123 -0
- ddd4py-0.1.0/README.md +104 -0
- ddd4py-0.1.0/Taskfile.yml +38 -0
- ddd4py-0.1.0/mise.toml +4 -0
- ddd4py-0.1.0/pyproject.toml +128 -0
- ddd4py-0.1.0/src/ddd4py/__init__.py +44 -0
- ddd4py-0.1.0/src/ddd4py/application/__init__.py +9 -0
- ddd4py-0.1.0/src/ddd4py/application/application_service_life_cycle.py +121 -0
- ddd4py-0.1.0/src/ddd4py/application/unit_of_work.py +53 -0
- ddd4py-0.1.0/src/ddd4py/domain/__init__.py +0 -0
- ddd4py-0.1.0/src/ddd4py/domain/model/__init__.py +11 -0
- ddd4py-0.1.0/src/ddd4py/domain/model/domain_event.py +90 -0
- ddd4py-0.1.0/src/ddd4py/domain/model/domain_registry.py +20 -0
- ddd4py-0.1.0/src/ddd4py/domain/model/event_context.py +27 -0
- ddd4py-0.1.0/src/ddd4py/event/__init__.py +5 -0
- ddd4py-0.1.0/src/ddd4py/event/event_context_provider.py +56 -0
- ddd4py-0.1.0/src/ddd4py/event/event_store.py +24 -0
- ddd4py-0.1.0/src/ddd4py/event/stored_event.py +74 -0
- ddd4py-0.1.0/src/ddd4py/exception/__init__.py +4 -0
- ddd4py-0.1.0/src/ddd4py/exception/error_code.py +50 -0
- ddd4py-0.1.0/src/ddd4py/exception/system_exception.py +16 -0
- ddd4py-0.1.0/src/ddd4py/module.py +81 -0
- ddd4py-0.1.0/src/ddd4py/notification/__init__.py +20 -0
- ddd4py-0.1.0/src/ddd4py/notification/consumed_notification.py +31 -0
- ddd4py-0.1.0/src/ddd4py/notification/consumed_notification_store.py +36 -0
- ddd4py-0.1.0/src/ddd4py/notification/notification.py +72 -0
- ddd4py-0.1.0/src/ddd4py/notification/notification_publisher.py +9 -0
- ddd4py-0.1.0/src/ddd4py/notification/notification_reader.py +33 -0
- ddd4py-0.1.0/src/ddd4py/notification/notification_serializer.py +17 -0
- ddd4py-0.1.0/src/ddd4py/notification/published_notification_tracker.py +60 -0
- ddd4py-0.1.0/src/ddd4py/notification/published_notification_tracker_store.py +17 -0
- ddd4py-0.1.0/src/ddd4py/port/__init__.py +0 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/__init__.py +0 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/__init__.py +5 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/exchange_listener.py +19 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/message_publisher.py +13 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/message_subscriber.py +112 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/stub/__init__.py +3 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/messaging/stub/message_publisher_stub.py +19 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/__init__.py +0 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/__init__.py +11 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/in_mem_consumed_notification_store.py +27 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/in_mem_event_store.py +31 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/in_mem_published_notification_tracker_store.py +20 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/inmem/in_mem_unit_of_work.py +37 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/sqlalchemy/__init__.py +4 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/sqlalchemy/session_preparer.py +28 -0
- ddd4py-0.1.0/src/ddd4py/port/adapter/persistence/sqlalchemy/sqlalchemy_unit_of_work.py +110 -0
- ddd4py-0.1.0/src/ddd4py/settings.py +40 -0
- ddd4py-0.1.0/src/ddd4py/testing/__init__.py +15 -0
- ddd4py-0.1.0/src/ddd4py/testing/contracts.py +111 -0
- ddd4py-0.1.0/src/ddd4py/testing/di.py +34 -0
- ddd4py-0.1.0/test/__init__.py +0 -0
- 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
- 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
- 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
- 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
|
ddd4py-0.1.0/.gitignore
ADDED
|
@@ -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,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"]
|