magic-pocket 0.2.0__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.
- magic_pocket-0.2.0.dist-info/METADATA +67 -0
- magic_pocket-0.2.0.dist-info/RECORD +31 -0
- magic_pocket-0.2.0.dist-info/WHEEL +4 -0
- magic_pocket-0.2.0.dist-info/licenses/LICENSE +21 -0
- pocket/__init__.py +3 -0
- pocket/command_handler.py +129 -0
- pocket/context.py +1167 -0
- pocket/django/__init__.py +9 -0
- pocket/django/context.py +172 -0
- pocket/django/db_backends/__init__.py +0 -0
- pocket/django/db_backends/rds/__init__.py +0 -0
- pocket/django/db_backends/rds/base.py +25 -0
- pocket/django/db_backends/rds/credentials.py +89 -0
- pocket/django/lambda_handlers.py +99 -0
- pocket/django/runtime.py +66 -0
- pocket/django/settings.py +79 -0
- pocket/django/spa_auth.py +146 -0
- pocket/django/storages.py +87 -0
- pocket/django/urls.py +51 -0
- pocket/django/utils.py +335 -0
- pocket/general_context.py +133 -0
- pocket/general_settings.py +71 -0
- pocket/permissions.py +216 -0
- pocket/resources/__init__.py +0 -0
- pocket/resources/aws/__init__.py +0 -0
- pocket/resources/aws/secretsmanager.py +91 -0
- pocket/resources/aws/ssm.py +98 -0
- pocket/resources/base.py +3 -0
- pocket/runtime.py +274 -0
- pocket/settings.py +880 -0
- pocket/utils.py +151 -0
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: magic-pocket
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Deploy system for Django projects.
|
|
5
|
+
Project-URL: Documentation, https://worgue.github.io/magic-pocket/
|
|
6
|
+
Project-URL: Source, https://github.com/worgue/magic-pocket
|
|
7
|
+
Author-email: Masaaki Yasui <yasu@worgue.com>
|
|
8
|
+
License: The MIT License (MIT)
|
|
9
|
+
|
|
10
|
+
Copyright (c) 2024 Masaaki Yasui
|
|
11
|
+
|
|
12
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
13
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
14
|
+
in the Software without restriction, including without limitation the rights
|
|
15
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
16
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
17
|
+
furnished to do so, subject to the following conditions:
|
|
18
|
+
|
|
19
|
+
The above copyright notice and this permission notice shall be included in
|
|
20
|
+
all copies or substantial portions of the Software.
|
|
21
|
+
|
|
22
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
23
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
24
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
25
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
26
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
27
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
28
|
+
THE SOFTWARE.
|
|
29
|
+
License-File: LICENSE
|
|
30
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
31
|
+
Requires-Python: >=3.10
|
|
32
|
+
Requires-Dist: apig-wsgi>=2.18.0
|
|
33
|
+
Requires-Dist: awslambdaric>=2.0.10
|
|
34
|
+
Requires-Dist: boto3>=1.34.28
|
|
35
|
+
Requires-Dist: django-storages!=1.14.3,>=1.14.2
|
|
36
|
+
Requires-Dist: pydantic-settings>=2.1.0
|
|
37
|
+
Requires-Dist: pydantic>=2.5.3
|
|
38
|
+
Requires-Dist: rich>=13.7.0
|
|
39
|
+
Requires-Dist: tomli>=1.1.0; python_version < '3.11'
|
|
40
|
+
Provides-Extra: django
|
|
41
|
+
Requires-Dist: django>=4.2.0; extra == 'django'
|
|
42
|
+
Provides-Extra: ses
|
|
43
|
+
Requires-Dist: django-ses>=4.0.0; extra == 'ses'
|
|
44
|
+
Provides-Extra: signing
|
|
45
|
+
Requires-Dist: cryptography>=41.0.0; extra == 'signing'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# Magic Pocket - Django Serverless Deployment
|
|
49
|
+
|
|
50
|
+
ドキュメントはこちら
|
|
51
|
+
https://worgue.github.io/magic-pocket/
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
magic-pocket の目標は、サーバレスDjangoです。Djangoを以下の環境にデプロイします。
|
|
55
|
+
|
|
56
|
+
- AWS Lambda
|
|
57
|
+
- Neon Postgres
|
|
58
|
+
- S3 storages
|
|
59
|
+
|
|
60
|
+
## Motivation
|
|
61
|
+
|
|
62
|
+
小規模な個人プロジェクトを、1人で複数同時に運用するため開発されたライブラリです。
|
|
63
|
+
気軽に作り、飽きたら放っておく、というスタイルで運用するため、以下の要件が最終目標になっています。
|
|
64
|
+
|
|
65
|
+
- サーバーの保守が不要
|
|
66
|
+
- 使わない間のコストが不要
|
|
67
|
+
- 環境を作る時にやる気を出す必要なし
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
pocket/__init__.py,sha256=zpEJ9DGm23z9S8qj-ify9ENGAOv_o_bo9nfQyJCl9G4,64
|
|
2
|
+
pocket/command_handler.py,sha256=rpwRQgLuMJuSZDzGdgIfdII-1JiVDx9ioANwGWcfmVs,6282
|
|
3
|
+
pocket/context.py,sha256=2vtIPAnmdVNWiq8OrL_DUqvR-xEE5HyU_FL40Fs2pe4,39415
|
|
4
|
+
pocket/general_context.py,sha256=jAUmn8uusRyh1OZZP5WUBjf1KXwhoxHfvGrIFovf8W0,4099
|
|
5
|
+
pocket/general_settings.py,sha256=FKS5-j5X9bD_albdadRtWwHKnpGhcE-PFdE4qeiQgPQ,2296
|
|
6
|
+
pocket/permissions.py,sha256=cny2JqZvdhpvV2XbJ-hD3EztiSTi3J7uoi4-cumQoaM,7698
|
|
7
|
+
pocket/runtime.py,sha256=8F1WbQCxuOC11Ik0neQDiosw4gqVL-ITUJfgZ7MR9E8,10487
|
|
8
|
+
pocket/settings.py,sha256=8ExOvJNkEj3VAw_VHLZARa5GJmJT5vw7-MivhTJgl7o,33262
|
|
9
|
+
pocket/utils.py,sha256=8qD4qjJlzMNNFAAO7yCdGrEFT9BQuBZi92w2et5CuVM,4293
|
|
10
|
+
pocket/django/__init__.py,sha256=nvNCBm-9BLVWmIUpmd_OETtWMFwQ7_fJZqE3PmLP_-4,177
|
|
11
|
+
pocket/django/context.py,sha256=Mvw8EX7Hk-X8NaiJYqrVM1J0Rqq9jRIx7gakmyMkI6k,6021
|
|
12
|
+
pocket/django/lambda_handlers.py,sha256=cXEzivSvxNXaxlAn6Qda6eWDNYE_1Dl0bMTlV60OJ9Y,3540
|
|
13
|
+
pocket/django/runtime.py,sha256=TtBKiWKLyBnPMB0aJRjHO-tsbXkwYibWiZ-Ws1BMlQk,2176
|
|
14
|
+
pocket/django/settings.py,sha256=LFe6t2MZuUzFmu-J8NeGQrqHsUIOlU8l4t7iyErye1w,2904
|
|
15
|
+
pocket/django/spa_auth.py,sha256=5093-a8uWeBPtZT_Pdp8p9IOBJRL8Gwqnidx98uDWmk,5349
|
|
16
|
+
pocket/django/storages.py,sha256=6nzoB25AJoI-O9ZLIq7vgBkD7vmK2KqDE62sQngFT-4,2854
|
|
17
|
+
pocket/django/urls.py,sha256=XZG1HDB8jqwWj8kkmcTW0QPzUSQGmuVp11MugQkZy6U,1581
|
|
18
|
+
pocket/django/utils.py,sha256=E6vm_LXH366xs2-B3xrz82xEX0j8Wk8518LF9T2JV68,12128
|
|
19
|
+
pocket/django/db_backends/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
20
|
+
pocket/django/db_backends/rds/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
21
|
+
pocket/django/db_backends/rds/base.py,sha256=YxrA_OXKN32wKvGtRIbiFgPXjjZ0ud_YdXC50S4hcxI,939
|
|
22
|
+
pocket/django/db_backends/rds/credentials.py,sha256=hyNwjrefaM5vbHLXXbtInnkkdhaq-OOLAj6_ZDnLQt4,3654
|
|
23
|
+
pocket/resources/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
24
|
+
pocket/resources/base.py,sha256=xOwos3J8RQgVxIpWw9IVYL3QKHHCAAtX6XJujsgBc6c,117
|
|
25
|
+
pocket/resources/aws/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
26
|
+
pocket/resources/aws/secretsmanager.py,sha256=WgNg1o3fVdamgm8qbM_w-pJFigGBJhuWzYtqEMs66tA,3496
|
|
27
|
+
pocket/resources/aws/ssm.py,sha256=ToxGnpJFiDev2h5p8Ce0eR13V8M4iCmEbxSMwYtUxGs,3771
|
|
28
|
+
magic_pocket-0.2.0.dist-info/METADATA,sha256=tk1pY28S1ZlN7q50DFEBdKAL2wAtZkwC81-12gM5Ve4,2865
|
|
29
|
+
magic_pocket-0.2.0.dist-info/WHEEL,sha256=mffPy8wBnZQn2VnJUU5jE99KsxaSfiyMHV9Yt0aLVxs,87
|
|
30
|
+
magic_pocket-0.2.0.dist-info/licenses/LICENSE,sha256=6sI-S9egZe33_sWOqLD7eyix1d7xlHn_oiB-3mlXrZw,1080
|
|
31
|
+
magic_pocket-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Masaaki Yasui
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
pocket/__init__.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
"""SQS イベント駆動の安全な command worker 基盤 ``BaseCommandHandler``.
|
|
2
|
+
|
|
3
|
+
長時間 job (provisioning 等) を wsgi (request/response) Lambda の中で background
|
|
4
|
+
thread + subprocess で動かすと、Lambda が **レスポンス返却後に execution
|
|
5
|
+
environment を freeze** するため、thread からの最終ステータス書き込みが freeze 跨ぎで
|
|
6
|
+
落ち、状態ストアが ``running`` のまま固着する。これを避けるため、job を「**SQS で
|
|
7
|
+
起動する別 Lambda invocation の本体**」として完走させる。worker は thread ではなく
|
|
8
|
+
invocation そのものなので freeze の影響を受けず、最後まで走り切って finalize できる。
|
|
9
|
+
|
|
10
|
+
安全境界:
|
|
11
|
+
|
|
12
|
+
- 実行ファイルは subclass の :meth:`BaseCommandHandler.build_argv` が固定し、argv は
|
|
13
|
+
**list** で **``shell=False``** で渡す。任意バイナリ / shell injection を不可にする
|
|
14
|
+
(= ``dangerous_shell_handler`` の安全な後継)。「何を実行させてよいか」の公開ポリシーは
|
|
15
|
+
enqueue する側 (認証済み API 等) が argv をどう組むかで決める。
|
|
16
|
+
- 出力 / lifecycle の永続化は sink hook (:meth:`on_start` / :meth:`on_output` /
|
|
17
|
+
:meth:`on_finish` / :meth:`on_crash`) に委譲する。基底は永続化先を知らない。
|
|
18
|
+
|
|
19
|
+
crash 時の挙動:
|
|
20
|
+
|
|
21
|
+
- 予期せぬ crash (spawn 失敗 / spec 不正 / sink エラー / OOM / timeout 等) で UI が
|
|
22
|
+
永遠に running を読まないよう、:meth:`_run` は ``try/finally`` + ``done_ok`` フラグで
|
|
23
|
+
「正常完了でないまま抜けた」ときだけ :meth:`on_crash` を呼ぶ。``except`` で握りつぶさ
|
|
24
|
+
ないので、例外はそのまま伝播し CloudWatch traceback + DLQ に残る (AGENTS.md
|
|
25
|
+
「曖昧な例外キャッチ禁止」と整合)。
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
import json
|
|
31
|
+
import os
|
|
32
|
+
import subprocess
|
|
33
|
+
import sys
|
|
34
|
+
import time
|
|
35
|
+
from abc import ABC, abstractmethod
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class BaseCommandHandler(ABC):
|
|
39
|
+
"""SQS event を受け、argv を invocation 本体として完走させる worker 基盤.
|
|
40
|
+
|
|
41
|
+
subclass は最低限 :meth:`build_argv` を実装すればよい。ステータス / 出力を永続化
|
|
42
|
+
したい場合は sink hook (:meth:`on_start` / :meth:`on_output` / :meth:`on_finish` /
|
|
43
|
+
:meth:`on_crash`) を override する (既定は no-op)。
|
|
44
|
+
|
|
45
|
+
``pocket.toml`` の ``[awscontainer.handlers.<key>]`` の ``command`` が、この
|
|
46
|
+
クラスのインスタンス (呼び出し可能) を dotted-path で指すように配線する。
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
#: 出力追記中の sink への書き込みを間引く throttle 間隔 (秒)。
|
|
50
|
+
#: 完了時 (:meth:`on_finish`) は throttle に関係なく確実に書く想定。
|
|
51
|
+
throttle: float = 1.0
|
|
52
|
+
|
|
53
|
+
def __call__(self, event, context):
|
|
54
|
+
"""SQS event source の Lambda entrypoint.
|
|
55
|
+
|
|
56
|
+
バッチ内の各 record (= 1 job) を順に完走させる。例外は catch せず伝播させ、
|
|
57
|
+
SQS の redrive / DLQ に委ねる。
|
|
58
|
+
"""
|
|
59
|
+
for record in event["Records"]:
|
|
60
|
+
self._run(json.loads(record["body"]))
|
|
61
|
+
|
|
62
|
+
def _run(self, spec: dict) -> None:
|
|
63
|
+
"""1 job 分のコマンドを完走させ、進捗 / 結果を sink hook 経由で永続化する."""
|
|
64
|
+
self.on_start(spec)
|
|
65
|
+
done_ok = False
|
|
66
|
+
try:
|
|
67
|
+
argv = self.build_argv(spec)
|
|
68
|
+
# PYTHONUNBUFFERED=1: subprocess 側の stdout を即 flush させ、Pipe 経由の
|
|
69
|
+
# block buffering でエラー時にログが消える事故を避ける。
|
|
70
|
+
env = {**os.environ, "PYTHONUNBUFFERED": "1"}
|
|
71
|
+
proc = subprocess.Popen(
|
|
72
|
+
argv,
|
|
73
|
+
stdout=subprocess.PIPE,
|
|
74
|
+
stderr=subprocess.STDOUT,
|
|
75
|
+
text=True,
|
|
76
|
+
bufsize=1,
|
|
77
|
+
env=env,
|
|
78
|
+
)
|
|
79
|
+
assert proc.stdout is not None
|
|
80
|
+
|
|
81
|
+
last_flush = 0.0
|
|
82
|
+
for line in proc.stdout:
|
|
83
|
+
now = time.time()
|
|
84
|
+
flush = now - last_flush >= self.throttle
|
|
85
|
+
self.on_output(spec, line.rstrip("\n"), flush=flush)
|
|
86
|
+
if flush:
|
|
87
|
+
last_flush = now
|
|
88
|
+
|
|
89
|
+
proc.wait()
|
|
90
|
+
# 本体内 (= 同一 invocation) での最終 finalize なので freeze の影響を
|
|
91
|
+
# 受けず確実。
|
|
92
|
+
self.on_finish(spec, proc.returncode)
|
|
93
|
+
done_ok = True
|
|
94
|
+
finally:
|
|
95
|
+
if not done_ok:
|
|
96
|
+
# 例外が伝播中 (= worker crash)。UI が failed を読めるよう sink に
|
|
97
|
+
# 記録する。正常 finalize 後 (done_ok=True) はここを通らない。
|
|
98
|
+
self.on_crash(spec, sys.exc_info()[1])
|
|
99
|
+
# 例外は finally を抜けて自然に伝播 (= re-raise) → CloudWatch + DLQ。
|
|
100
|
+
|
|
101
|
+
@abstractmethod
|
|
102
|
+
def build_argv(self, spec: dict) -> list[str]:
|
|
103
|
+
"""job spec を実行する argv (list[str]) に変換する.
|
|
104
|
+
|
|
105
|
+
安全境界はここ: 実行ファイルを自分の CLI に固定し、shell を介さず list で
|
|
106
|
+
渡す。``spec`` の中身 (どの引数を許すか) の検証も必要ならここで行う。
|
|
107
|
+
"""
|
|
108
|
+
...
|
|
109
|
+
|
|
110
|
+
# --- sink hooks ---
|
|
111
|
+
# 既定はいずれも stdout への print (= CloudWatch Logs に残る)。subclass が
|
|
112
|
+
# override してステータス / 出力を任意のストア (S3 snapshot 等) に永続化する。
|
|
113
|
+
# build_argv だけ実装した subclass でも、最低限ログは CloudWatch に出る。
|
|
114
|
+
|
|
115
|
+
def on_start(self, spec: dict) -> None:
|
|
116
|
+
"""job 開始時 (subprocess 起動前)。running 状態の初期化に使う."""
|
|
117
|
+
print(f"start job: {spec}")
|
|
118
|
+
|
|
119
|
+
def on_output(self, spec: dict, line: str, *, flush: bool) -> None:
|
|
120
|
+
"""出力 1 行ごと。``flush`` が True のとき永続化先へ書き出す想定."""
|
|
121
|
+
print(line)
|
|
122
|
+
|
|
123
|
+
def on_finish(self, spec: dict, exit_code: int) -> None:
|
|
124
|
+
"""subprocess 完走時 (exit_code は 0 以外もあり得る = job の失敗)."""
|
|
125
|
+
print(f"finished: exit {exit_code}")
|
|
126
|
+
|
|
127
|
+
def on_crash(self, spec: dict, exc: BaseException | None) -> None:
|
|
128
|
+
"""worker が正常完了せず抜けたとき (crash)。例外はこの後 re-raise される."""
|
|
129
|
+
print(f"crashed: {type(exc).__name__}: {exc}")
|