fastship-py 0.4.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.
- fastship_py-0.4.0/.github/workflows/ci.yml +50 -0
- fastship_py-0.4.0/.github/workflows/claude.yml +56 -0
- fastship_py-0.4.0/.github/workflows/publish.yml +75 -0
- fastship_py-0.4.0/.gitignore +133 -0
- fastship_py-0.4.0/CONTRIBUTING.md +69 -0
- fastship_py-0.4.0/LICENSE +21 -0
- fastship_py-0.4.0/PKG-INFO +176 -0
- fastship_py-0.4.0/README.md +155 -0
- fastship_py-0.4.0/Taskfile.yml +32 -0
- fastship_py-0.4.0/lefthook.yml +20 -0
- fastship_py-0.4.0/mise.toml +4 -0
- fastship_py-0.4.0/pyproject.toml +171 -0
- fastship_py-0.4.0/src/fastship/__init__.py +13 -0
- fastship_py-0.4.0/src/fastship/common/__init__.py +5 -0
- fastship_py-0.4.0/src/fastship/common/application/__init__.py +9 -0
- fastship_py-0.4.0/src/fastship/common/application/application_service_life_cycle.py +121 -0
- fastship_py-0.4.0/src/fastship/common/application/unit_of_work.py +53 -0
- fastship_py-0.4.0/src/fastship/common/domain/__init__.py +0 -0
- fastship_py-0.4.0/src/fastship/common/domain/model/__init__.py +11 -0
- fastship_py-0.4.0/src/fastship/common/domain/model/domain_event.py +90 -0
- fastship_py-0.4.0/src/fastship/common/domain/model/domain_registry.py +22 -0
- fastship_py-0.4.0/src/fastship/common/domain/model/event_context.py +27 -0
- fastship_py-0.4.0/src/fastship/common/event/__init__.py +5 -0
- fastship_py-0.4.0/src/fastship/common/event/event_context_provider.py +56 -0
- fastship_py-0.4.0/src/fastship/common/event/event_store.py +24 -0
- fastship_py-0.4.0/src/fastship/common/event/stored_event.py +73 -0
- fastship_py-0.4.0/src/fastship/common/exception/__init__.py +4 -0
- fastship_py-0.4.0/src/fastship/common/exception/error_code.py +50 -0
- fastship_py-0.4.0/src/fastship/common/exception/system_exception.py +16 -0
- fastship_py-0.4.0/src/fastship/common/module.py +82 -0
- fastship_py-0.4.0/src/fastship/common/notification/__init__.py +20 -0
- fastship_py-0.4.0/src/fastship/common/notification/consumed_notification.py +31 -0
- fastship_py-0.4.0/src/fastship/common/notification/consumed_notification_store.py +36 -0
- fastship_py-0.4.0/src/fastship/common/notification/notification.py +72 -0
- fastship_py-0.4.0/src/fastship/common/notification/notification_publisher.py +9 -0
- fastship_py-0.4.0/src/fastship/common/notification/notification_reader.py +33 -0
- fastship_py-0.4.0/src/fastship/common/notification/notification_serializer.py +17 -0
- fastship_py-0.4.0/src/fastship/common/notification/published_notification_tracker.py +60 -0
- fastship_py-0.4.0/src/fastship/common/notification/published_notification_tracker_store.py +17 -0
- fastship_py-0.4.0/src/fastship/common/port/__init__.py +0 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/__init__.py +0 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/messaging/__init__.py +5 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/messaging/exchange_listener.py +19 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/messaging/message_publisher.py +13 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/messaging/message_subscriber.py +117 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/messaging/stub/__init__.py +3 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/messaging/stub/message_publisher_stub.py +19 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/__init__.py +0 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/inmem/__init__.py +11 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/inmem/in_mem_consumed_notification_store.py +27 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/inmem/in_mem_event_store.py +31 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/inmem/in_mem_published_notification_tracker_store.py +20 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/inmem/in_mem_unit_of_work.py +37 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/sqlalchemy/__init__.py +15 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/sqlalchemy/database.py +27 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/sqlalchemy/database_settings.py +19 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/sqlalchemy/routing_session.py +165 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/sqlalchemy/session_preparer.py +33 -0
- fastship_py-0.4.0/src/fastship/common/port/adapter/persistence/sqlalchemy/sqlalchemy_unit_of_work.py +161 -0
- fastship_py-0.4.0/src/fastship/common/settings.py +34 -0
- fastship_py-0.4.0/src/fastship/common/testing/__init__.py +13 -0
- fastship_py-0.4.0/src/fastship/common/testing/contracts.py +111 -0
- fastship_py-0.4.0/src/fastship/di/__init__.py +6 -0
- fastship_py-0.4.0/src/fastship/di/container.py +53 -0
- fastship_py-0.4.0/src/fastship/di/di.py +76 -0
- fastship_py-0.4.0/src/fastship/linter/__init__.py +0 -0
- fastship_py-0.4.0/src/fastship/linter/architecture.py +145 -0
- fastship_py-0.4.0/src/fastship/migration/__init__.py +15 -0
- fastship_py-0.4.0/src/fastship/migration/metadata.py +22 -0
- fastship_py-0.4.0/test/__init__.py +0 -0
- fastship_py-0.4.0/test/test_/343/203/206/343/203/274/343/203/226/343/203/253/343/201/256/345/237/272/345/272/225.py +113 -0
- fastship_py-0.4.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 +147 -0
- fastship_py-0.4.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 +47 -0
- fastship_py-0.4.0/test/test_/343/203/236/343/202/244/343/202/260/343/203/254/343/203/274/343/202/267/343/203/247/343/203/263.py +114 -0
- fastship_py-0.4.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 +99 -0
- fastship_py-0.4.0/test/test_/350/252/255/343/201/277/346/233/270/343/201/215/343/201/256/346/214/257/343/202/212/345/210/206/343/201/221.py +500 -0
- fastship_py-0.4.0/uv.lock +742 -0
|
@@ -0,0 +1,50 @@
|
|
|
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@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
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: フォーマット検査
|
|
37
|
+
# 整形は ruff format が唯一の基準。差分があれば `task style:fix` で解消する。
|
|
38
|
+
run: uv run ruff format --check .
|
|
39
|
+
|
|
40
|
+
- name: Ruff リント
|
|
41
|
+
run: uv run ruff check .
|
|
42
|
+
|
|
43
|
+
- name: 型チェック
|
|
44
|
+
run: uv run mypy
|
|
45
|
+
|
|
46
|
+
- name: テスト
|
|
47
|
+
run: uv run pytest
|
|
48
|
+
|
|
49
|
+
- name: Import Linter
|
|
50
|
+
run: uv run lint-imports --no-logo
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: "🧠 Claude"
|
|
2
|
+
|
|
3
|
+
# 組織共通の Claude ワークフロー (theindiehacker/devops) を呼び出す caller
|
|
4
|
+
on:
|
|
5
|
+
issue_comment:
|
|
6
|
+
types: [created]
|
|
7
|
+
pull_request_review_comment:
|
|
8
|
+
types: [created]
|
|
9
|
+
pull_request_review:
|
|
10
|
+
types: [submitted]
|
|
11
|
+
issues:
|
|
12
|
+
types: [opened]
|
|
13
|
+
|
|
14
|
+
# write は呼び出し先の job に渡す分だけを job 側で与える
|
|
15
|
+
permissions: {}
|
|
16
|
+
|
|
17
|
+
jobs:
|
|
18
|
+
code-review:
|
|
19
|
+
if: github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/code-review')
|
|
20
|
+
permissions:
|
|
21
|
+
contents: read
|
|
22
|
+
actions: read
|
|
23
|
+
pull-requests: write
|
|
24
|
+
issues: write
|
|
25
|
+
id-token: write
|
|
26
|
+
uses: theindiehacker/devops/.github/workflows/claude-code-review.yml@a6db12cef36ce69e0d76ce26c380c2255d4764ea # v1.4.1
|
|
27
|
+
secrets:
|
|
28
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
29
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
30
|
+
|
|
31
|
+
security-review:
|
|
32
|
+
# 実際の pwn request 対策 (ロール判定など) は呼び出し先 claude-security-review.yml の job if で行う。
|
|
33
|
+
# ここは code-review job と同様、呼び出し先を起動するかどうかの粗いフィルタ
|
|
34
|
+
if: github.event_name == 'issue_comment' && startsWith(github.event.comment.body, '/security-review')
|
|
35
|
+
permissions:
|
|
36
|
+
contents: read
|
|
37
|
+
actions: read
|
|
38
|
+
pull-requests: write
|
|
39
|
+
issues: write
|
|
40
|
+
id-token: write
|
|
41
|
+
uses: theindiehacker/devops/.github/workflows/claude-security-review.yml@a6db12cef36ce69e0d76ce26c380c2255d4764ea # v1.4.1
|
|
42
|
+
secrets:
|
|
43
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
44
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
45
|
+
|
|
46
|
+
claude:
|
|
47
|
+
permissions:
|
|
48
|
+
contents: write
|
|
49
|
+
actions: read
|
|
50
|
+
pull-requests: write
|
|
51
|
+
issues: write
|
|
52
|
+
id-token: write
|
|
53
|
+
uses: theindiehacker/devops/.github/workflows/claude.yml@a6db12cef36ce69e0d76ce26c380c2255d4764ea # v1.4.1
|
|
54
|
+
secrets:
|
|
55
|
+
CLAUDE_CODE_OAUTH_TOKEN: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
|
56
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
@@ -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@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
|
|
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/fastship-py
|
|
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/
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Development
|
|
2
|
+
|
|
3
|
+
## Set up
|
|
4
|
+
```bash
|
|
5
|
+
task init # 依存インストール
|
|
6
|
+
task test # テスト
|
|
7
|
+
task style:check # ruff format / ruff / mypy / import-linter (自分自身への契約検査)
|
|
8
|
+
task style:fix # 整形 + lint の自動修正
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Debug
|
|
12
|
+
|
|
13
|
+
ローカルの変更を、実際のリポジトリに撃って確かめる手順。
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
cd path/to/対象プロジェクト
|
|
17
|
+
uv add --dev --editable path/to/fastship-py # 一度だけ
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Release
|
|
21
|
+
|
|
22
|
+
PyPI への公開は GitHub Release をトリガーに、[Trusted Publishing (OIDC)](https://docs.pypi.org/trusted-publishers/) で自動実行される。
|
|
23
|
+
API トークンはリポジトリに置かない。
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
# 1. pyproject.toml の version を上げて main へマージする
|
|
27
|
+
# 2. その version と同じタグで Release を作る (v プレフィックス付き)
|
|
28
|
+
gh release create v0.1.0 --generate-notes
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
タグと `pyproject.toml` の version が食い違うとワークフローは公開前に落ちる。
|
|
32
|
+
`__version__` は `pyproject.toml` から読むため、バージョンを書き換える箇所は `pyproject.toml` の 1 行だけ。
|
|
33
|
+
|
|
34
|
+
## CI
|
|
35
|
+
|
|
36
|
+
PR には以下のチェックが走る。
|
|
37
|
+
|
|
38
|
+
| ワークフロー | 検知するもの |
|
|
39
|
+
|:---|:---|
|
|
40
|
+
| `ci.yml` | 整形 (ruff format) / lint (ruff) / 型 (mypy) / テスト / アーキテクチャ規約 (import-linter) |
|
|
41
|
+
| `claude.yml` | 検知ではなく呼び出し用。PR に `/code-review`・`/security-review` とコメント、または `@claude` にメンションで Claude を起動する |
|
|
42
|
+
| org の必須ワークフロー | シークレット混入 (Gitleaks) / SAST (Semgrep) / 依存パッケージの脆弱性・IaC の設定ミス (Trivy) / GitHub Actions 定義 (zizmor / ghalint / actionlint) など |
|
|
43
|
+
|
|
44
|
+
org の必須ワークフローは [theindiehacker/devops](https://github.com/theindiehacker/devops) の定義を org ルールセットで全リポジトリに適用しているため、本リポジトリには置かない。
|
|
45
|
+
|
|
46
|
+
### `ci.yml` を必須にする (リポジトリ管理者向け)
|
|
47
|
+
|
|
48
|
+
ワークフローを置いただけではマージはブロックされない。
|
|
49
|
+
Settings → Rules → Rulesets → New branch ruleset で、
|
|
50
|
+
**Target branches** を `Include default branch`、
|
|
51
|
+
**Require status checks to pass** に `検査` を登録する。
|
|
52
|
+
|
|
53
|
+
> チェック名は job 名がそのまま出たもの。
|
|
54
|
+
> job 名を変えると必須チェックの名前も変わり、PR が永久に pending になるため変更しない。
|
|
55
|
+
|
|
56
|
+
### 検知の抑制
|
|
57
|
+
|
|
58
|
+
誤検知の抑制は、org ルールセット「セキュリティ設定変更の承認必須化」の対象である
|
|
59
|
+
設定ファイル (`.gitleaksignore` / `.trivyignore` / `.semgrepignore` / `.github/zizmor.yml`) に限定する。
|
|
60
|
+
各ツールのインライン抑制コメント (gitleaks の allow / Semgrep の nosem /
|
|
61
|
+
Trivy と zizmor の ignore) は承認ゲートを迂回するため、
|
|
62
|
+
各ワークフローで明示的に無効化、または存在自体を禁止している。
|
|
63
|
+
|
|
64
|
+
> ここでトークンを原文のまま書いていないのは、Trivy ワークフローの禁止チェックが
|
|
65
|
+
> リポジトリ全体を grep するため、この文書自身が検知されてしまうため。
|
|
66
|
+
|
|
67
|
+
これらの設定ファイルは**検出が実際に出て、誤検知だと判断できた時にだけ**作る。
|
|
68
|
+
先回りして空ファイルを置かないこと。特に `.semgrepignore` は、存在するだけで
|
|
69
|
+
Semgrep の組み込み既定除外リストを置き換えてしまう。
|
|
@@ -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.
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: fastship-py
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: モジュラモノリス + DDD のカーネル。集約・ドメインイベント・UnitOfWork・トランザクショナル outbox/inbox を業務語彙ゼロで提供する
|
|
5
|
+
Project-URL: Homepage, https://github.com/theindiehacker/fastship-py
|
|
6
|
+
Project-URL: Repository, https://github.com/theindiehacker/fastship-py
|
|
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: injector<1.0.0,>=0.22.0
|
|
13
|
+
Requires-Dist: pydantic-settings<3.0.0,>=2.14.2
|
|
14
|
+
Requires-Dist: pydantic<3.0.0,>=2.13.0
|
|
15
|
+
Requires-Dist: pytz>=2026.2
|
|
16
|
+
Provides-Extra: linter
|
|
17
|
+
Requires-Dist: import-linter<3.0.0,>=2.14; extra == 'linter'
|
|
18
|
+
Provides-Extra: sqlalchemy
|
|
19
|
+
Requires-Dist: sqlalchemy<3.0.0,>=2.0.51; extra == 'sqlalchemy'
|
|
20
|
+
Description-Content-Type: text/markdown
|
|
21
|
+
|
|
22
|
+
# 🚀 fastship-py
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
uv add fastship-py
|
|
26
|
+
uv add "fastship-py[sqlalchemy]" # SQLAlchemy アダプタも使う場合
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## 📚 How To
|
|
30
|
+
### 💾 Persistence
|
|
31
|
+
|
|
32
|
+
<details><summary><b>💾 テーブルクラス ( SQLAlchemy ) の実装</b></summary>
|
|
33
|
+
|
|
34
|
+
`〇〇.port.adapter.persistence.repository.sqlalchemy.user.driver.users_table_row.py`
|
|
35
|
+
```python
|
|
36
|
+
from sqlalchemy import String
|
|
37
|
+
from sqlalchemy.orm import Mapped, mapped_column
|
|
38
|
+
|
|
39
|
+
# NOTE: テーブルクラスは `DataBase` を継承して作ること
|
|
40
|
+
from fastship.common.port.adapter.persistence.sqlalchemy import DataBase
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class UsersTableRow(DataBase):
|
|
44
|
+
__tablename__ = "users"
|
|
45
|
+
|
|
46
|
+
id: Mapped[str] = mapped_column(String(36), primary_key=True)
|
|
47
|
+
name: Mapped[str] = mapped_column(String(50), index=True) # index 名は ix_users_name
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`〇〇.port.adapter.persistence.repository.sqlalchemy.user.driver.__init__.py`
|
|
51
|
+
```python
|
|
52
|
+
from .users_table_row import UsersTableRow
|
|
53
|
+
# 他のテーブルクラス
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
テーブルクラスを足したら、そのモジュールの `tables` にドライバーモジュールのパスを足す
|
|
57
|
+
|
|
58
|
+
```python
|
|
59
|
+
class 〇〇(AppModule):
|
|
60
|
+
@property
|
|
61
|
+
@override
|
|
62
|
+
def tables(self) -> list[str]:
|
|
63
|
+
return ["〇〇.port.adapter.persistence.repository.sqlalchemy.user.driver"]
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
</details>
|
|
67
|
+
|
|
68
|
+
<details><summary><b>🚚 マイグレーション (Alembic)</b></summary>
|
|
69
|
+
|
|
70
|
+
事前にマイグレーション用のパッケージを生成
|
|
71
|
+
```bash
|
|
72
|
+
uv add alembic
|
|
73
|
+
uv run alembic init migration
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
`migration/env.py`:
|
|
77
|
+
```python
|
|
78
|
+
# alembic init の生成物から、target_metadata = None をこの 3 行に差し替える
|
|
79
|
+
|
|
80
|
+
from fastship.migration import load_metadata
|
|
81
|
+
from core import Backend # 全モジュールを束ねた CompositeModule
|
|
82
|
+
|
|
83
|
+
target_metadata = load_metadata(Backend())
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
</details>
|
|
87
|
+
|
|
88
|
+
<details><summary><b>🔀 リードレプリカへの読み書き振り分け (SQLAlchemy)</b></summary>
|
|
89
|
+
|
|
90
|
+
接続先はインフラが環境変数で注入する (Secret Manager の値は実行基盤で環境変数として公開する)。アプリ側では設定しない。
|
|
91
|
+
|
|
92
|
+
| 環境変数 | 内容 |
|
|
93
|
+
|:---|:---|
|
|
94
|
+
| `DATABASE_WRITER_URL` | プライマリ。必須 |
|
|
95
|
+
| `DATABASE_READER_URL` | リードレプリカ (Read Pool のエンドポイント)。未設定ならすべてプライマリへ送る |
|
|
96
|
+
|
|
97
|
+
```python
|
|
98
|
+
from fastship.common.port.adapter.persistence.sqlalchemy import SQLAlchemyUnitOfWork
|
|
99
|
+
|
|
100
|
+
unit_of_work = SQLAlchemyUnitOfWork.from_settings(pool_pre_ping=True)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
トランザクション (`unit_of_work.session()`) は、読み取りも含めてすべてプライマリで実行する。
|
|
104
|
+
レプリカの遅延した値を元に更新すると、プライマリの新しい値を上書きしてしまう (lost update) ため。
|
|
105
|
+
更新系の生 SQL (`text()`) もここで実行する。
|
|
106
|
+
|
|
107
|
+
トランザクション外の読み取り (`unit_of_work.query()`) は、以下の規則でリーダーとライターに振り分ける。
|
|
108
|
+
|
|
109
|
+
| クエリ | 送り先 |
|
|
110
|
+
|:---|:---|
|
|
111
|
+
| 読み取り (`select()` / `session.get()` / `text()`) | リーダー |
|
|
112
|
+
| 書き込み (flush / `insert()` / `update()` / `delete()`) | ライター |
|
|
113
|
+
| `SELECT ... FOR UPDATE` (`with_for_update()`) | ライター |
|
|
114
|
+
| 一度でもライターへ送ったセッションの以降のクエリ | ライター (書き込み直後に古いデータを読まない) |
|
|
115
|
+
| `session.use_writer()` を呼んだ後のクエリ | ライター (レプリカ遅延を許容できない読み取り) |
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
with unit_of_work.query() as session:
|
|
119
|
+
session.use_writer()
|
|
120
|
+
session.get(...)
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
認可の判定 (無効化された API キー、停止されたユーザー、取り消された権限など) は、レプリカ遅延の間に
|
|
124
|
+
古い状態で許可しないよう、`session.use_writer()` を呼んでプライマリから読むこと。
|
|
125
|
+
|
|
126
|
+
Read Pool のノード数を増減しても、エンドポイントは変わらないためアプリ側の変更は要らない。
|
|
127
|
+
`SessionPreparer` (RLS の `SET LOCAL` など) は、振り分け先のコネクションそれぞれに適用される。
|
|
128
|
+
|
|
129
|
+
</details>
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
### 🛠️ Other
|
|
133
|
+
|
|
134
|
+
<details><summary><b>📦 DI コンテナ</b></summary>
|
|
135
|
+
|
|
136
|
+
登録方法:
|
|
137
|
+
```python
|
|
138
|
+
from sqlalchemy import create_engine
|
|
139
|
+
|
|
140
|
+
from fastship.common.application import UnitOfWork
|
|
141
|
+
from fastship.common.port.adapter.persistence.inmem import InMemUnitOfWork
|
|
142
|
+
from fastship.common.port.adapter.persistence.sqlalchemy import SQLAlchemyUnitOfWork
|
|
143
|
+
from fastship.di import DI, DIContainer
|
|
144
|
+
|
|
145
|
+
engine = create_engine("postgresql+psycopg://...")
|
|
146
|
+
|
|
147
|
+
DIContainer.instance().register(
|
|
148
|
+
DI.of(UnitOfWork, {"InMem": InMemUnitOfWork()}, SQLAlchemyUnitOfWork(engine)),
|
|
149
|
+
)
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
利用方法:
|
|
153
|
+
```python
|
|
154
|
+
from fastship.di import DI, DIContainer
|
|
155
|
+
|
|
156
|
+
# `DI_FOR_PY=InMem` なら InMemUnitOfWork のインタンスを取得
|
|
157
|
+
# `DI_FOR_PY=` なら SQLAlchemyUnitOfWork のインタンスを取得
|
|
158
|
+
unit_of_work = DIContainer.instance().resolve(UnitOfWork)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
from injector import inject
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
class 〇〇ApplicationService:
|
|
166
|
+
@inject
|
|
167
|
+
def __init__(self, 〇〇_repository: 〇〇Repository) -> None: ...
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from fastship.common.domain.model import DomainRegistry
|
|
172
|
+
|
|
173
|
+
〇〇_repository = DomainRegistry.resolve(〇〇Repository)
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
</details>
|