SessionSmith 2.0.0__tar.gz → 2.2.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.
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/PKG-INFO +89 -6
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/__init__.py +40 -1
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/core.py +9 -4
- sessionsmith-2.2.0/SessionSmith/crypto.py +205 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/error_handling.py +1 -1
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/exceptions.py +44 -2
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/i18n.py +5 -3
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/info.py +0 -1
- sessionsmith-2.2.0/SessionSmith/locking.py +385 -0
- sessionsmith-2.2.0/SessionSmith/logging_config.py +194 -0
- sessionsmith-2.2.0/SessionSmith/remote_backends.py +326 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/ssm.py +1122 -414
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/tracer.py +2 -2
- sessionsmith-2.2.0/SessionSmith/validation.py +158 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith.egg-info/PKG-INFO +89 -6
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith.egg-info/SOURCES.txt +15 -1
- sessionsmith-2.2.0/SessionSmith.egg-info/requires.txt +32 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/pyproject.toml +67 -1
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/readme.md +75 -0
- sessionsmith-2.2.0/setup.py +4 -0
- sessionsmith-2.2.0/tests/test_crypto.py +95 -0
- sessionsmith-2.2.0/tests/test_locking.py +454 -0
- sessionsmith-2.2.0/tests/test_logging_config.py +106 -0
- sessionsmith-2.2.0/tests/test_merge_checkout_features.py +224 -0
- sessionsmith-2.2.0/tests/test_remote_backends.py +166 -0
- sessionsmith-2.2.0/tests/test_security.py +243 -0
- sessionsmith-2.2.0/tests/test_ssm_bugfixes.py +239 -0
- sessionsmith-2.2.0/tests/test_ssm_e2e.py +466 -0
- sessionsmith-2.2.0/tests/test_verify.py +66 -0
- sessionsmith-2.0.0/SessionSmith.egg-info/requires.txt +0 -15
- sessionsmith-2.0.0/setup.py +0 -71
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/LICENSE +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/cli.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/compare.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/formats.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/jupyter_utils.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/manager.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/py.typed +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/resource_manager.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/serializers.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/utils.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/visualizer.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/visualizer_arrays.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith/visualizer_generic.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith.egg-info/dependency_links.txt +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith.egg-info/entry_points.txt +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/SessionSmith.egg-info/top_level.txt +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/setup.cfg +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/tests/test_cli.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/tests/test_core.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.2.0}/tests/test_ssm.py +0 -0
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SessionSmith
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.2.0
|
|
4
4
|
Summary: Git-style session management for Python. Save, restore, and track your variables with ease.
|
|
5
|
-
Home-page: https://github.com/yut0takagi/SessionSmith
|
|
6
|
-
Author: YutoTAKAGI
|
|
7
5
|
Author-email: YutoTAKAGI <yutotkg.1040@gmail.com>
|
|
8
6
|
License: MIT
|
|
9
7
|
Project-URL: Homepage, https://github.com/yut0takagi/SessionSmith
|
|
@@ -33,6 +31,15 @@ Description-Content-Type: text/markdown
|
|
|
33
31
|
License-File: LICENSE
|
|
34
32
|
Provides-Extra: visualization
|
|
35
33
|
Requires-Dist: matplotlib>=3.5.0; extra == "visualization"
|
|
34
|
+
Provides-Extra: crypto
|
|
35
|
+
Requires-Dist: cryptography>=3.4; extra == "crypto"
|
|
36
|
+
Provides-Extra: s3
|
|
37
|
+
Requires-Dist: boto3>=1.26; extra == "s3"
|
|
38
|
+
Provides-Extra: gcs
|
|
39
|
+
Requires-Dist: google-cloud-storage>=2.0; extra == "gcs"
|
|
40
|
+
Provides-Extra: cloud
|
|
41
|
+
Requires-Dist: boto3>=1.26; extra == "cloud"
|
|
42
|
+
Requires-Dist: google-cloud-storage>=2.0; extra == "cloud"
|
|
36
43
|
Provides-Extra: dev
|
|
37
44
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
45
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -41,12 +48,13 @@ Requires-Dist: black>=23.0.0; extra == "dev"
|
|
|
41
48
|
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
42
49
|
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
43
50
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
51
|
+
Requires-Dist: cryptography>=3.4; extra == "dev"
|
|
44
52
|
Provides-Extra: all
|
|
45
53
|
Requires-Dist: matplotlib>=3.5.0; extra == "all"
|
|
46
|
-
|
|
47
|
-
|
|
54
|
+
Requires-Dist: cryptography>=3.4; extra == "all"
|
|
55
|
+
Requires-Dist: boto3>=1.26; extra == "all"
|
|
56
|
+
Requires-Dist: google-cloud-storage>=2.0; extra == "all"
|
|
48
57
|
Dynamic: license-file
|
|
49
|
-
Dynamic: requires-python
|
|
50
58
|
|
|
51
59
|
# SessionSmith
|
|
52
60
|
|
|
@@ -78,6 +86,9 @@ Dynamic: requires-python
|
|
|
78
86
|
- 🚀 **拡張機能対応**: Cursor/VSCode拡張機能でコードを書かずに実行可能
|
|
79
87
|
- 🌐 **多言語対応**: 日本語・英語のエラーメッセージに対応
|
|
80
88
|
- 🛡️ **堅牢なエラーハンドリング**: リトライ、詳細なエラー情報、コンテキスト管理
|
|
89
|
+
- ☁️ **クラウドリモート**(v2.1.0): S3 / GCS / HTTP へ `push` / `pull`
|
|
90
|
+
- 🔐 **暗号化・改ざん検出**(v2.1.0): 認証付き暗号でのエクスポート、HMAC 署名による検証
|
|
91
|
+
- 📝 **構造化ロギング**(v2.1.0): ログレベル・ファイル出力・JSON ログ
|
|
81
92
|
|
|
82
93
|
## インストール
|
|
83
94
|
|
|
@@ -105,6 +116,18 @@ pip install SessionSmith[visualization]
|
|
|
105
116
|
pip install matplotlib
|
|
106
117
|
```
|
|
107
118
|
|
|
119
|
+
オプション機能(v2.1.0):
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
pip install SessionSmith[crypto] # 暗号化(cryptography)
|
|
123
|
+
pip install SessionSmith[s3] # S3 リモート(boto3)
|
|
124
|
+
pip install SessionSmith[gcs] # GCS リモート(google-cloud-storage)
|
|
125
|
+
pip install SessionSmith[cloud] # S3 + GCS
|
|
126
|
+
pip install SessionSmith[all] # すべて
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
> 署名(HMAC)・構造化ロギングは追加依存なしで利用できます。
|
|
130
|
+
|
|
108
131
|
## クイックスタート
|
|
109
132
|
|
|
110
133
|
### SSM - Git風セッション管理(推奨)
|
|
@@ -228,6 +251,61 @@ lang = get_language() # 'ja' または 'en'
|
|
|
228
251
|
エラーメッセージや情報メッセージが設定した言語で表示されます。
|
|
229
252
|
SSMが初期化されている場合、言語設定は自動的に `.ssm/config` に保存されます。
|
|
230
253
|
|
|
254
|
+
### クラウド / URL リモート(v2.1.0)
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
from SessionSmith import ssm
|
|
258
|
+
|
|
259
|
+
ssm.init()
|
|
260
|
+
ssm.commit("experiment v1")
|
|
261
|
+
|
|
262
|
+
# クラウドリモートを登録して push / pull
|
|
263
|
+
ssm.remote_add("cloud", "s3://my-bucket/experiments") # S3
|
|
264
|
+
# ssm.remote_add("cloud", "gs://my-bucket/experiments") # GCS
|
|
265
|
+
# ssm.remote_add("cloud", "file:///shared/ssm-remote") # 共有ディレクトリ
|
|
266
|
+
ssm.push("cloud", "main")
|
|
267
|
+
|
|
268
|
+
# 別マシン / 別リポジトリから取得
|
|
269
|
+
ssm.pull("cloud", "main")
|
|
270
|
+
|
|
271
|
+
# HTTP(S) 越しの読み取り(pull のみ)
|
|
272
|
+
ssm.remote_add("mirror", "https://example.com/ssm-repo")
|
|
273
|
+
ssm.pull("mirror", "main")
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
### 暗号化・改ざん検出(v2.1.0)
|
|
277
|
+
|
|
278
|
+
```python
|
|
279
|
+
from SessionSmith import ssm
|
|
280
|
+
|
|
281
|
+
# --- 暗号化(要 pip install SessionSmith[crypto])---
|
|
282
|
+
ssm.export("backup.pkl", password="my-secret") # 暗号化してエクスポート
|
|
283
|
+
ssm.import_session("backup.pkl", password="my-secret") # 復号してインポート
|
|
284
|
+
ssm.push("cloud", "main", password="my-secret") # リモート上のデータを暗号化
|
|
285
|
+
|
|
286
|
+
# --- 改ざん検出(HMAC 署名・追加依存なし)---
|
|
287
|
+
ssm.config("sign_key", "team-secret") # 署名鍵を設定(環境変数 SESSIONSMITH_SIGN_KEY でも可)
|
|
288
|
+
ssm.commit("signed snapshot") # 以降のコミットに署名が付与される
|
|
289
|
+
|
|
290
|
+
result = ssm.verify() # 整合性(再ハッシュ)と署名を検証
|
|
291
|
+
# {'integrity_ok': True, 'signed': True, 'signature_ok': True, 'issues': []}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### 構造化ロギング(v2.1.0)
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
from SessionSmith import setup_logging, enable_debug, set_log_level
|
|
298
|
+
|
|
299
|
+
setup_logging(level="INFO", log_file="ssm.log") # ファイル出力(ローテーション付き)
|
|
300
|
+
setup_logging(level="INFO", json_format=True) # JSON 構造化ログ
|
|
301
|
+
enable_debug() # デバッグモード
|
|
302
|
+
|
|
303
|
+
# 環境変数でも設定可能:
|
|
304
|
+
# SESSIONSMITH_LOG_LEVEL=DEBUG
|
|
305
|
+
# SESSIONSMITH_LOG_FILE=ssm.log
|
|
306
|
+
# SESSIONSMITH_LOG_JSON=1
|
|
307
|
+
```
|
|
308
|
+
|
|
231
309
|
### レガシーAPI(後方互換性)
|
|
232
310
|
|
|
233
311
|
> ⚠️ 以下のAPIは後方互換性のために残されています。新規開発では `ssm` の使用を推奨します。
|
|
@@ -341,6 +419,7 @@ Python notebookで複数のファイルからインポートしたり、複数
|
|
|
341
419
|
- 📈 [アルゴリズムトレーサー](docs/algorithm-tracer.md) - トレース・可視化機能
|
|
342
420
|
- 🌐 [国際化(i18n)ガイド](docs/i18n-guide.md) - 多言語対応(日本語・英語)
|
|
343
421
|
- 📚 [APIリファレンス](docs/api-reference.md) - 全APIの詳細
|
|
422
|
+
- ⏱️ [ベンチマーク](benchmarks/README.md) - 大規模セッション・チェックポイント・メモリ計測(issue #31)
|
|
344
423
|
|
|
345
424
|
## 使用例
|
|
346
425
|
|
|
@@ -407,6 +486,10 @@ SessionSmithには、Cursor/VSCode用の拡張機能が用意されています
|
|
|
407
486
|
|
|
408
487
|
### 機能
|
|
409
488
|
|
|
489
|
+
- 🌳 **Session Graph(v0.2.0〜)**: `.ssm/` のコミット履歴を gitgraph 風に可視化。
|
|
490
|
+
ブランチのレーン色分け、マージの分岐・合流、ブランチ/タグ/HEADバッジ、コミット詳細
|
|
491
|
+
(変数一覧・署名状態)、GUI からの Checkout / Branch / Tag / Commit に対応
|
|
492
|
+
- 🗂 **Sessions ビュー(v0.2.0〜)**: アクティビティバーにブランチ・タグ・コミットのツリー表示
|
|
410
493
|
- ✅ **Save Session**: 現在のPythonセッション(変数)を保存
|
|
411
494
|
- ✅ **Load Session**: セッションファイルを選択して変数を復元
|
|
412
495
|
- ✅ **Show Session Info**: セッションファイルの情報を表示
|
|
@@ -39,6 +39,16 @@ from .compare import compare_sessions, print_comparison
|
|
|
39
39
|
|
|
40
40
|
# 後方互換性のためのAPI(非推奨)
|
|
41
41
|
from .core import load_session, save_session
|
|
42
|
+
|
|
43
|
+
# セキュリティ(暗号化・署名)
|
|
44
|
+
from .crypto import (
|
|
45
|
+
HAS_CRYPTOGRAPHY,
|
|
46
|
+
CryptoError,
|
|
47
|
+
decrypt_data,
|
|
48
|
+
encrypt_data,
|
|
49
|
+
sign_data,
|
|
50
|
+
verify_signature,
|
|
51
|
+
)
|
|
42
52
|
from .error_handling import (
|
|
43
53
|
ErrorHandler,
|
|
44
54
|
error_context,
|
|
@@ -76,6 +86,17 @@ from .exceptions import (
|
|
|
76
86
|
)
|
|
77
87
|
from .i18n import Language, get_language, set_language, t, translate
|
|
78
88
|
from .info import get_session_info, list_session_variables, print_session_info
|
|
89
|
+
|
|
90
|
+
# ロギング設定
|
|
91
|
+
from .logging_config import (
|
|
92
|
+
configure_from_env as _configure_logging_from_env,
|
|
93
|
+
)
|
|
94
|
+
from .logging_config import (
|
|
95
|
+
enable_debug,
|
|
96
|
+
get_log_level,
|
|
97
|
+
set_log_level,
|
|
98
|
+
setup_logging,
|
|
99
|
+
)
|
|
79
100
|
from .manager import SessionManager
|
|
80
101
|
from .serializers import CustomSerializer
|
|
81
102
|
from .ssm import (
|
|
@@ -97,7 +118,13 @@ from .tracer import AlgorithmTracer
|
|
|
97
118
|
from .utils import verify_session
|
|
98
119
|
from .visualizer import print_trace_summary, visualize_algorithm_trace
|
|
99
120
|
|
|
100
|
-
__version__ = "2.
|
|
121
|
+
__version__ = "2.2.0"
|
|
122
|
+
|
|
123
|
+
# 環境変数からロギングを自動設定(SESSIONSMITH_LOG_LEVEL / SESSIONSMITH_LOG_FILE)
|
|
124
|
+
try:
|
|
125
|
+
_configure_logging_from_env()
|
|
126
|
+
except Exception: # pragma: no cover - ロギング設定失敗で import を妨げない
|
|
127
|
+
pass
|
|
101
128
|
|
|
102
129
|
__all__ = [
|
|
103
130
|
# 主要API(推奨)
|
|
@@ -134,6 +161,18 @@ __all__ = [
|
|
|
134
161
|
"ErrorHandler",
|
|
135
162
|
"set_default_error_handler",
|
|
136
163
|
"get_default_error_handler",
|
|
164
|
+
# ロギング
|
|
165
|
+
"setup_logging",
|
|
166
|
+
"set_log_level",
|
|
167
|
+
"get_log_level",
|
|
168
|
+
"enable_debug",
|
|
169
|
+
# セキュリティ(暗号化・署名)
|
|
170
|
+
"encrypt_data",
|
|
171
|
+
"decrypt_data",
|
|
172
|
+
"sign_data",
|
|
173
|
+
"verify_signature",
|
|
174
|
+
"CryptoError",
|
|
175
|
+
"HAS_CRYPTOGRAPHY",
|
|
137
176
|
# ブランチ・マージ・タグ・リモート機能
|
|
138
177
|
"branch",
|
|
139
178
|
"checkout_branch",
|
|
@@ -46,9 +46,14 @@ def _validate_file_path(file_path: Union[str, Path]) -> Path:
|
|
|
46
46
|
if not isinstance(file_path, (str, Path)):
|
|
47
47
|
raise TypeError(f"file_path must be str or Path, got {type(file_path).__name__}")
|
|
48
48
|
|
|
49
|
-
|
|
49
|
+
path_str = str(file_path)
|
|
50
|
+
|
|
51
|
+
if not path_str.strip():
|
|
50
52
|
raise ValueError("file_path cannot be empty")
|
|
51
53
|
|
|
54
|
+
if any(ord(ch) < 0x20 or ord(ch) == 0x7F for ch in path_str):
|
|
55
|
+
raise ValueError("file_path must not contain control characters")
|
|
56
|
+
|
|
52
57
|
return Path(file_path)
|
|
53
58
|
|
|
54
59
|
|
|
@@ -540,11 +545,11 @@ def load_session(
|
|
|
540
545
|
session = {k: v for k, v in session.items() if k not in exclude}
|
|
541
546
|
|
|
542
547
|
# グローバル変数に更新
|
|
543
|
-
|
|
548
|
+
loaded_names: list[str] = []
|
|
544
549
|
for k, v in session.items():
|
|
545
550
|
try:
|
|
546
551
|
globals_dict[k] = v
|
|
547
|
-
|
|
552
|
+
loaded_names.append(k)
|
|
548
553
|
if verbose:
|
|
549
554
|
print(f"Loaded variable: {k} ({type(v).__name__})")
|
|
550
555
|
except Exception as e:
|
|
@@ -552,6 +557,6 @@ def load_session(
|
|
|
552
557
|
warnings.warn(f"Failed to load variable '{k}': {str(e)}", UserWarning, stacklevel=2)
|
|
553
558
|
|
|
554
559
|
if verbose:
|
|
555
|
-
print(f"Loaded {len(
|
|
560
|
+
print(f"Loaded {len(loaded_names)} variables")
|
|
556
561
|
|
|
557
562
|
return session
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"""
|
|
2
|
+
暗号化・署名モジュール(セキュリティ機能)
|
|
3
|
+
|
|
4
|
+
このモジュールは2つの独立した機能を提供します:
|
|
5
|
+
|
|
6
|
+
1. **整合性・改ざん検出(署名)**: HMAC-SHA256 を使用。Python標準ライブラリ
|
|
7
|
+
のみで動作し、追加の依存関係は不要です。コミットやエクスポートしたデータの
|
|
8
|
+
改ざんを検出できます。
|
|
9
|
+
|
|
10
|
+
2. **暗号化**: 認証付き暗号(Fernet / AES-128-CBC + HMAC)を使用。`cryptography`
|
|
11
|
+
パッケージが必要です(``pip install SessionSmith[crypto]``)。パスワードから
|
|
12
|
+
PBKDF2-HMAC-SHA256 で鍵を導出します。
|
|
13
|
+
|
|
14
|
+
Note:
|
|
15
|
+
暗号化はエクスポート/インポートやリモートとの同期など、データが SessionSmith
|
|
16
|
+
の管理下を離れる「境界」での利用を想定しています。pickle ファイルを平文のまま
|
|
17
|
+
共有・バックアップするリスクを軽減します。
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import base64
|
|
21
|
+
import hashlib
|
|
22
|
+
import hmac
|
|
23
|
+
import os
|
|
24
|
+
import struct
|
|
25
|
+
from typing import Optional
|
|
26
|
+
|
|
27
|
+
# 暗号化用のオプショナル依存
|
|
28
|
+
# ImportError だけでなく、ネイティブ拡張(cffi/rust)の読み込み失敗など
|
|
29
|
+
# 環境依存の例外も握りつぶし、暗号化機能を無効化して degrade させる。
|
|
30
|
+
try:
|
|
31
|
+
from cryptography.fernet import Fernet, InvalidToken
|
|
32
|
+
|
|
33
|
+
HAS_CRYPTOGRAPHY = True
|
|
34
|
+
except BaseException: # noqa: BLE001 # pragma: no cover - 依存がない/壊れている環境向け
|
|
35
|
+
# ImportError に加え、ネイティブ拡張(cffi/pyo3)の読み込み失敗による
|
|
36
|
+
# PanicException(BaseException 派生)等もここで握りつぶし degrade させる。
|
|
37
|
+
HAS_CRYPTOGRAPHY = False
|
|
38
|
+
Fernet = None # type: ignore
|
|
39
|
+
InvalidToken = Exception # type: ignore
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
# 暗号化済みデータを識別するためのマジックヘッダー
|
|
43
|
+
MAGIC = b"SSMENC01"
|
|
44
|
+
# 鍵導出のデフォルト反復回数(PBKDF2)
|
|
45
|
+
DEFAULT_ITERATIONS = 200_000
|
|
46
|
+
# ソルト長(バイト)
|
|
47
|
+
SALT_SIZE = 16
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
class CryptoError(Exception):
|
|
51
|
+
"""暗号化・復号・署名検証に関するエラー"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class CryptoDependencyError(CryptoError):
|
|
55
|
+
"""暗号化に必要な依存パッケージが不足している場合のエラー"""
|
|
56
|
+
|
|
57
|
+
def __init__(self) -> None:
|
|
58
|
+
super().__init__(
|
|
59
|
+
"Encryption requires the 'cryptography' package. "
|
|
60
|
+
"Install it with: pip install SessionSmith[crypto]"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
# ========== 署名(HMAC-SHA256, 標準ライブラリのみ) ==========
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def sign_data(data: bytes, key: str) -> str:
|
|
68
|
+
"""
|
|
69
|
+
データに HMAC-SHA256 署名を付与します(改ざん検出用)。
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
data: 署名対象のバイト列
|
|
73
|
+
key: 署名鍵(秘密鍵)
|
|
74
|
+
|
|
75
|
+
Returns:
|
|
76
|
+
str: 16進数の署名文字列
|
|
77
|
+
"""
|
|
78
|
+
if not isinstance(data, bytes):
|
|
79
|
+
raise TypeError("data must be bytes")
|
|
80
|
+
return hmac.new(key.encode("utf-8"), data, hashlib.sha256).hexdigest()
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def verify_signature(data: bytes, signature: str, key: str) -> bool:
|
|
84
|
+
"""
|
|
85
|
+
HMAC-SHA256 署名を検証します(タイミング攻撃に耐性のある比較)。
|
|
86
|
+
|
|
87
|
+
Args:
|
|
88
|
+
data: 検証対象のバイト列
|
|
89
|
+
signature: 期待される署名(16進数文字列)
|
|
90
|
+
key: 署名鍵
|
|
91
|
+
|
|
92
|
+
Returns:
|
|
93
|
+
bool: 署名が一致すれば True
|
|
94
|
+
"""
|
|
95
|
+
if not signature:
|
|
96
|
+
return False
|
|
97
|
+
expected = sign_data(data, key)
|
|
98
|
+
return hmac.compare_digest(expected, signature)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
# ========== 暗号化(Fernet, オプショナル) ==========
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def _derive_fernet_key(password: str, salt: bytes, iterations: int = DEFAULT_ITERATIONS) -> bytes:
|
|
105
|
+
"""
|
|
106
|
+
パスワードから Fernet 用の鍵(url-safe base64 でエンコードされた32バイト)を導出します。
|
|
107
|
+
|
|
108
|
+
PBKDF2-HMAC-SHA256 を標準ライブラリで実行するため、cryptography の鍵導出機能には
|
|
109
|
+
依存しません。
|
|
110
|
+
"""
|
|
111
|
+
dk = hashlib.pbkdf2_hmac("sha256", password.encode("utf-8"), salt, iterations, dklen=32)
|
|
112
|
+
return base64.urlsafe_b64encode(dk)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def is_encrypted(blob: bytes) -> bool:
|
|
116
|
+
"""データが SessionSmith により暗号化されたものか判定します。"""
|
|
117
|
+
return isinstance(blob, bytes) and blob[: len(MAGIC)] == MAGIC
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def encrypt_data(data: bytes, password: str, iterations: int = DEFAULT_ITERATIONS) -> bytes:
|
|
121
|
+
"""
|
|
122
|
+
データを暗号化します(認証付き暗号)。
|
|
123
|
+
|
|
124
|
+
出力フォーマット::
|
|
125
|
+
|
|
126
|
+
MAGIC(8) | iterations(uint32 BE) | salt(16) | Fernet token(...)
|
|
127
|
+
|
|
128
|
+
Args:
|
|
129
|
+
data: 平文のバイト列
|
|
130
|
+
password: 暗号化パスワード
|
|
131
|
+
iterations: PBKDF2 の反復回数
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
bytes: 暗号化されたバイト列(``is_encrypted`` が True を返す形式)
|
|
135
|
+
|
|
136
|
+
Raises:
|
|
137
|
+
CryptoDependencyError: cryptography パッケージが無い場合
|
|
138
|
+
"""
|
|
139
|
+
if not HAS_CRYPTOGRAPHY:
|
|
140
|
+
raise CryptoDependencyError()
|
|
141
|
+
if not password:
|
|
142
|
+
raise CryptoError("Password must not be empty")
|
|
143
|
+
if not isinstance(data, bytes):
|
|
144
|
+
raise TypeError("data must be bytes")
|
|
145
|
+
|
|
146
|
+
salt = os.urandom(SALT_SIZE)
|
|
147
|
+
key = _derive_fernet_key(password, salt, iterations)
|
|
148
|
+
token = Fernet(key).encrypt(data)
|
|
149
|
+
header = MAGIC + struct.pack(">I", iterations) + salt
|
|
150
|
+
return header + token
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def decrypt_data(blob: bytes, password: str) -> bytes:
|
|
154
|
+
"""
|
|
155
|
+
``encrypt_data`` で暗号化されたデータを復号します。
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
blob: 暗号化されたバイト列
|
|
159
|
+
password: 復号パスワード
|
|
160
|
+
|
|
161
|
+
Returns:
|
|
162
|
+
bytes: 復号された平文
|
|
163
|
+
|
|
164
|
+
Raises:
|
|
165
|
+
CryptoDependencyError: cryptography パッケージが無い場合
|
|
166
|
+
CryptoError: フォーマット不正・パスワード誤り・改ざんを検出した場合
|
|
167
|
+
"""
|
|
168
|
+
if not HAS_CRYPTOGRAPHY:
|
|
169
|
+
raise CryptoDependencyError()
|
|
170
|
+
if not is_encrypted(blob):
|
|
171
|
+
raise CryptoError("Data is not in SessionSmith encrypted format")
|
|
172
|
+
|
|
173
|
+
header_len = len(MAGIC) + 4 + SALT_SIZE
|
|
174
|
+
if len(blob) < header_len:
|
|
175
|
+
raise CryptoError("Encrypted data is truncated or corrupted")
|
|
176
|
+
|
|
177
|
+
iterations = struct.unpack(">I", blob[len(MAGIC) : len(MAGIC) + 4])[0]
|
|
178
|
+
salt = blob[len(MAGIC) + 4 : header_len]
|
|
179
|
+
token = blob[header_len:]
|
|
180
|
+
|
|
181
|
+
key = _derive_fernet_key(password, salt, iterations)
|
|
182
|
+
try:
|
|
183
|
+
return Fernet(key).decrypt(token)
|
|
184
|
+
except InvalidToken as e:
|
|
185
|
+
raise CryptoError(
|
|
186
|
+
"Failed to decrypt: wrong password or the data has been tampered with"
|
|
187
|
+
) from e
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def maybe_decrypt(blob: bytes, password: Optional[str]) -> bytes:
|
|
191
|
+
"""
|
|
192
|
+
暗号化されていれば復号し、そうでなければそのまま返します。
|
|
193
|
+
|
|
194
|
+
Args:
|
|
195
|
+
blob: 読み込んだバイト列
|
|
196
|
+
password: パスワード(None で暗号化されていない場合はそのまま)
|
|
197
|
+
|
|
198
|
+
Returns:
|
|
199
|
+
bytes: 平文のバイト列
|
|
200
|
+
"""
|
|
201
|
+
if is_encrypted(blob):
|
|
202
|
+
if not password:
|
|
203
|
+
raise CryptoError("Data is encrypted but no password was provided")
|
|
204
|
+
return decrypt_data(blob, password)
|
|
205
|
+
return blob
|
|
@@ -118,7 +118,7 @@ def safe_execute(
|
|
|
118
118
|
func: Callable[..., T],
|
|
119
119
|
*args: Any,
|
|
120
120
|
default: Optional[T] = None,
|
|
121
|
-
on_error: Callable[[Exception], Optional[T]] = None,
|
|
121
|
+
on_error: Optional[Callable[[Exception], Optional[T]]] = None,
|
|
122
122
|
**kwargs: Any
|
|
123
123
|
) -> Optional[T]:
|
|
124
124
|
"""
|
|
@@ -9,7 +9,8 @@ SessionSmith カスタム例外クラス
|
|
|
9
9
|
│ ├── SSMNotInitializedError
|
|
10
10
|
│ ├── SSMCommitNotFoundError
|
|
11
11
|
│ ├── SSMNoCommitsError
|
|
12
|
-
│
|
|
12
|
+
│ ├── SSMConfigError
|
|
13
|
+
│ └── SSMLockError
|
|
13
14
|
├── SessionError (セッション操作関連)
|
|
14
15
|
│ ├── SessionSaveError
|
|
15
16
|
│ ├── SessionLoadError
|
|
@@ -134,6 +135,40 @@ class SSMRemoteNotFoundError(SSMError):
|
|
|
134
135
|
super().__init__(message, details={"remote_name": remote_name})
|
|
135
136
|
|
|
136
137
|
|
|
138
|
+
class SSMLockError(SSMError):
|
|
139
|
+
"""`.ssm` リポジトリのプロセス間ロック取得がタイムアウトした場合の例外"""
|
|
140
|
+
|
|
141
|
+
def __init__(
|
|
142
|
+
self,
|
|
143
|
+
ssm_path: str,
|
|
144
|
+
holder_pid: Optional[int] = None,
|
|
145
|
+
timeout: Optional[float] = None,
|
|
146
|
+
):
|
|
147
|
+
self.ssm_path = ssm_path
|
|
148
|
+
self.holder_pid = holder_pid
|
|
149
|
+
self.timeout = timeout
|
|
150
|
+
|
|
151
|
+
if holder_pid:
|
|
152
|
+
holder_desc = f"currently held by PID {holder_pid}"
|
|
153
|
+
else:
|
|
154
|
+
holder_desc = "holder could not be determined"
|
|
155
|
+
timeout_desc = f"{timeout:.1f}s" if timeout is not None else "the configured timeout"
|
|
156
|
+
|
|
157
|
+
message = (
|
|
158
|
+
f"Timed out after {timeout_desc} waiting for the lock on '{ssm_path}' "
|
|
159
|
+
f"({holder_desc}). Another process or thread is likely writing to this "
|
|
160
|
+
f".ssm repository. If no other process is actually using it, the lock "
|
|
161
|
+
f"file ('.lock') may be stale from a crashed process; SessionSmith "
|
|
162
|
+
f"automatically reclaims stale locks once the holder process is "
|
|
163
|
+
f"confirmed dead or the lock exceeds its max age, so retrying shortly "
|
|
164
|
+
f"usually resolves this."
|
|
165
|
+
)
|
|
166
|
+
super().__init__(
|
|
167
|
+
message,
|
|
168
|
+
details={"ssm_path": ssm_path, "holder_pid": holder_pid, "timeout": timeout},
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
|
|
137
172
|
class SSMMergeConflictError(SSMError):
|
|
138
173
|
"""マージコンフリクトが発生した場合の例外"""
|
|
139
174
|
|
|
@@ -141,7 +176,14 @@ class SSMMergeConflictError(SSMError):
|
|
|
141
176
|
self.branch_name = branch_name
|
|
142
177
|
self.conflicts = conflicts
|
|
143
178
|
i18n = _get_i18n()
|
|
144
|
-
|
|
179
|
+
# コンフリクトした変数名をメッセージ本文にも含める(呼び出し側が
|
|
180
|
+
# str(exc) だけを見ても、どの変数が衝突したか分かるようにするため)
|
|
181
|
+
message = i18n.translate(
|
|
182
|
+
"error.merge_conflict",
|
|
183
|
+
branch_name=branch_name,
|
|
184
|
+
count=len(conflicts),
|
|
185
|
+
vars=", ".join(conflicts),
|
|
186
|
+
)
|
|
145
187
|
super().__init__(message, details={"branch_name": branch_name, "conflicts": conflicts})
|
|
146
188
|
|
|
147
189
|
|
|
@@ -53,7 +53,7 @@ _TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
|
53
53
|
"error.remote_no_url": "リモート '{remote_name}' にURLが設定されていません",
|
|
54
54
|
"error.remote_repository_not_found": "リモートリポジトリが見つかりません: {remote_url}",
|
|
55
55
|
"error.tag_no_commit": "タグ '{tag_name}' にコミットが設定されていません",
|
|
56
|
-
"error.merge_conflict": "マージコンフリクトが発生しました: ブランチ '{branch_name}' で {count} 個のコンフリクト",
|
|
56
|
+
"error.merge_conflict": "マージコンフリクトが発生しました: ブランチ '{branch_name}' で {count} 個のコンフリクト ({vars})",
|
|
57
57
|
|
|
58
58
|
# 警告メッセージ
|
|
59
59
|
"warn.large_variable": "大きな変数が検出されました: '{name}' ({size_mb:.1f}MB)",
|
|
@@ -62,6 +62,7 @@ _TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
|
62
62
|
"warn.checkpoint_failed": "チェックポイントの保存に失敗しました: {reason}",
|
|
63
63
|
"warn.continuous_mode_unavailable": "常時記録モードはJupyter/IPython環境でのみ利用可能です",
|
|
64
64
|
"warn.variable_conflict": "変数名の衝突が検出されました: ファイル '{previous_file}' と '{current_file}' で同じ変数名 ({vars}) が使用されています。複数のファイルから同じ変数名を使用する場合は注意してください。",
|
|
65
|
+
"warn.merge_conflict": "マージコンフリクトを検出しました: ブランチ '{branch_name}' で変数 {vars} が共通祖先から両側で異なる値に変更されています。マージ呼び出し時点でセッションに存在する値が採用されます(last-writer-wins)。",
|
|
65
66
|
|
|
66
67
|
# 情報メッセージ
|
|
67
68
|
"info.session_saved": "セッションを保存しました: {file_path} ({size:,} bytes, 形式: {format})",
|
|
@@ -122,7 +123,7 @@ _TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
|
122
123
|
"error.remote_no_url": "Remote '{remote_name}' has no URL",
|
|
123
124
|
"error.remote_repository_not_found": "Remote repository not found: {remote_url}",
|
|
124
125
|
"error.tag_no_commit": "Tag '{tag_name}' has no commit",
|
|
125
|
-
"error.merge_conflict": "Merge conflict: {count} conflicts in branch '{branch_name}'",
|
|
126
|
+
"error.merge_conflict": "Merge conflict: {count} conflicts in branch '{branch_name}' ({vars})",
|
|
126
127
|
|
|
127
128
|
# Warning messages
|
|
128
129
|
"warn.large_variable": "Large variable detected: '{name}' ({size_mb:.1f}MB)",
|
|
@@ -131,6 +132,7 @@ _TRANSLATIONS: dict[str, dict[str, str]] = {
|
|
|
131
132
|
"warn.checkpoint_failed": "Checkpoint save failed: {reason}",
|
|
132
133
|
"warn.continuous_mode_unavailable": "Continuous mode is only available in Jupyter/IPython environment",
|
|
133
134
|
"warn.variable_conflict": "Variable name conflict detected: same variable names ({vars}) used in '{previous_file}' and '{current_file}'. Be careful when using the same variable names from multiple files.",
|
|
135
|
+
"warn.merge_conflict": "Merge conflict detected: variables {vars} were changed differently on both sides since the common ancestor in branch '{branch_name}'. The value currently in the session at merge time will be used (last-writer-wins).",
|
|
134
136
|
"warn.disk_warning": "Disk space warning: {usage_percent:.1f}% used ({free_mb:.1f}MB free)",
|
|
135
137
|
"warn.disk_critical": "Disk space critical: {usage_percent:.1f}% used ({free_mb:.1f}MB free)",
|
|
136
138
|
"warn.memory_warning": "Memory usage warning: {usage_percent:.1f}% used ({used_mb:.1f}MB used, {available_mb:.1f}MB available)",
|
|
@@ -210,7 +212,7 @@ def get_language() -> str:
|
|
|
210
212
|
if _current_language == Language.AUTO:
|
|
211
213
|
return detect_language()
|
|
212
214
|
else:
|
|
213
|
-
return _current_language.value
|
|
215
|
+
return str(_current_language.value)
|
|
214
216
|
|
|
215
217
|
|
|
216
218
|
def set_language(lang: Union[str, Language], save_to_ssm: bool = True) -> None:
|
|
@@ -31,7 +31,6 @@ def _load_session_file(file_path: Union[str, Path], format: Optional[str] = None
|
|
|
31
31
|
if not file_path.is_file():
|
|
32
32
|
raise ValueError(f"'{file_path}' is not a file.")
|
|
33
33
|
|
|
34
|
-
compression: Optional[str] = None
|
|
35
34
|
from .formats import detect_format, load_hdf5, load_json, load_msgpack, load_pickle
|
|
36
35
|
|
|
37
36
|
session: dict[str, Any] = {}
|