SessionSmith 2.0.0__tar.gz → 2.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.
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/PKG-INFO +88 -1
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/__init__.py +40 -1
- sessionsmith-2.1.0/SessionSmith/crypto.py +205 -0
- sessionsmith-2.1.0/SessionSmith/logging_config.py +194 -0
- sessionsmith-2.1.0/SessionSmith/remote_backends.py +277 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/ssm.py +362 -32
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/PKG-INFO +88 -1
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/SOURCES.txt +8 -1
- sessionsmith-2.1.0/SessionSmith.egg-info/requires.txt +32 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/pyproject.toml +10 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/readme.md +74 -0
- sessionsmith-2.1.0/tests/test_crypto.py +95 -0
- sessionsmith-2.1.0/tests/test_logging_config.py +106 -0
- sessionsmith-2.1.0/tests/test_remote_backends.py +166 -0
- sessionsmith-2.1.0/tests/test_verify.py +66 -0
- sessionsmith-2.0.0/SessionSmith.egg-info/requires.txt +0 -15
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/LICENSE +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/cli.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/compare.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/core.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/error_handling.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/exceptions.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/formats.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/i18n.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/info.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/jupyter_utils.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/manager.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/py.typed +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/resource_manager.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/serializers.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/tracer.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/utils.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/visualizer.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/visualizer_arrays.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/visualizer_generic.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/dependency_links.txt +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/entry_points.txt +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/top_level.txt +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/setup.cfg +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/setup.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/tests/test_cli.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/tests/test_core.py +0 -0
- {sessionsmith-2.0.0 → sessionsmith-2.1.0}/tests/test_ssm.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: SessionSmith
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.0
|
|
4
4
|
Summary: Git-style session management for Python. Save, restore, and track your variables with ease.
|
|
5
5
|
Home-page: https://github.com/yut0takagi/SessionSmith
|
|
6
6
|
Author: YutoTAKAGI
|
|
@@ -33,6 +33,15 @@ Description-Content-Type: text/markdown
|
|
|
33
33
|
License-File: LICENSE
|
|
34
34
|
Provides-Extra: visualization
|
|
35
35
|
Requires-Dist: matplotlib>=3.5.0; extra == "visualization"
|
|
36
|
+
Provides-Extra: crypto
|
|
37
|
+
Requires-Dist: cryptography>=3.4; extra == "crypto"
|
|
38
|
+
Provides-Extra: s3
|
|
39
|
+
Requires-Dist: boto3>=1.26; extra == "s3"
|
|
40
|
+
Provides-Extra: gcs
|
|
41
|
+
Requires-Dist: google-cloud-storage>=2.0; extra == "gcs"
|
|
42
|
+
Provides-Extra: cloud
|
|
43
|
+
Requires-Dist: boto3>=1.26; extra == "cloud"
|
|
44
|
+
Requires-Dist: google-cloud-storage>=2.0; extra == "cloud"
|
|
36
45
|
Provides-Extra: dev
|
|
37
46
|
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
38
47
|
Requires-Dist: pytest-cov>=4.0.0; extra == "dev"
|
|
@@ -41,8 +50,12 @@ Requires-Dist: black>=23.0.0; extra == "dev"
|
|
|
41
50
|
Requires-Dist: isort>=5.12.0; extra == "dev"
|
|
42
51
|
Requires-Dist: mypy>=1.0.0; extra == "dev"
|
|
43
52
|
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
53
|
+
Requires-Dist: cryptography>=3.4; extra == "dev"
|
|
44
54
|
Provides-Extra: all
|
|
45
55
|
Requires-Dist: matplotlib>=3.5.0; extra == "all"
|
|
56
|
+
Requires-Dist: cryptography>=3.4; extra == "all"
|
|
57
|
+
Requires-Dist: boto3>=1.26; extra == "all"
|
|
58
|
+
Requires-Dist: google-cloud-storage>=2.0; extra == "all"
|
|
46
59
|
Dynamic: author
|
|
47
60
|
Dynamic: home-page
|
|
48
61
|
Dynamic: license-file
|
|
@@ -78,6 +91,9 @@ Dynamic: requires-python
|
|
|
78
91
|
- 🚀 **拡張機能対応**: Cursor/VSCode拡張機能でコードを書かずに実行可能
|
|
79
92
|
- 🌐 **多言語対応**: 日本語・英語のエラーメッセージに対応
|
|
80
93
|
- 🛡️ **堅牢なエラーハンドリング**: リトライ、詳細なエラー情報、コンテキスト管理
|
|
94
|
+
- ☁️ **クラウドリモート**(v2.1.0): S3 / GCS / HTTP へ `push` / `pull`
|
|
95
|
+
- 🔐 **暗号化・改ざん検出**(v2.1.0): 認証付き暗号でのエクスポート、HMAC 署名による検証
|
|
96
|
+
- 📝 **構造化ロギング**(v2.1.0): ログレベル・ファイル出力・JSON ログ
|
|
81
97
|
|
|
82
98
|
## インストール
|
|
83
99
|
|
|
@@ -105,6 +121,18 @@ pip install SessionSmith[visualization]
|
|
|
105
121
|
pip install matplotlib
|
|
106
122
|
```
|
|
107
123
|
|
|
124
|
+
オプション機能(v2.1.0):
|
|
125
|
+
|
|
126
|
+
```bash
|
|
127
|
+
pip install SessionSmith[crypto] # 暗号化(cryptography)
|
|
128
|
+
pip install SessionSmith[s3] # S3 リモート(boto3)
|
|
129
|
+
pip install SessionSmith[gcs] # GCS リモート(google-cloud-storage)
|
|
130
|
+
pip install SessionSmith[cloud] # S3 + GCS
|
|
131
|
+
pip install SessionSmith[all] # すべて
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
> 署名(HMAC)・構造化ロギングは追加依存なしで利用できます。
|
|
135
|
+
|
|
108
136
|
## クイックスタート
|
|
109
137
|
|
|
110
138
|
### SSM - Git風セッション管理(推奨)
|
|
@@ -228,6 +256,61 @@ lang = get_language() # 'ja' または 'en'
|
|
|
228
256
|
エラーメッセージや情報メッセージが設定した言語で表示されます。
|
|
229
257
|
SSMが初期化されている場合、言語設定は自動的に `.ssm/config` に保存されます。
|
|
230
258
|
|
|
259
|
+
### クラウド / URL リモート(v2.1.0)
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from SessionSmith import ssm
|
|
263
|
+
|
|
264
|
+
ssm.init()
|
|
265
|
+
ssm.commit("experiment v1")
|
|
266
|
+
|
|
267
|
+
# クラウドリモートを登録して push / pull
|
|
268
|
+
ssm.remote_add("cloud", "s3://my-bucket/experiments") # S3
|
|
269
|
+
# ssm.remote_add("cloud", "gs://my-bucket/experiments") # GCS
|
|
270
|
+
# ssm.remote_add("cloud", "file:///shared/ssm-remote") # 共有ディレクトリ
|
|
271
|
+
ssm.push("cloud", "main")
|
|
272
|
+
|
|
273
|
+
# 別マシン / 別リポジトリから取得
|
|
274
|
+
ssm.pull("cloud", "main")
|
|
275
|
+
|
|
276
|
+
# HTTP(S) 越しの読み取り(pull のみ)
|
|
277
|
+
ssm.remote_add("mirror", "https://example.com/ssm-repo")
|
|
278
|
+
ssm.pull("mirror", "main")
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
### 暗号化・改ざん検出(v2.1.0)
|
|
282
|
+
|
|
283
|
+
```python
|
|
284
|
+
from SessionSmith import ssm
|
|
285
|
+
|
|
286
|
+
# --- 暗号化(要 pip install SessionSmith[crypto])---
|
|
287
|
+
ssm.export("backup.pkl", password="my-secret") # 暗号化してエクスポート
|
|
288
|
+
ssm.import_session("backup.pkl", password="my-secret") # 復号してインポート
|
|
289
|
+
ssm.push("cloud", "main", password="my-secret") # リモート上のデータを暗号化
|
|
290
|
+
|
|
291
|
+
# --- 改ざん検出(HMAC 署名・追加依存なし)---
|
|
292
|
+
ssm.config("sign_key", "team-secret") # 署名鍵を設定(環境変数 SESSIONSMITH_SIGN_KEY でも可)
|
|
293
|
+
ssm.commit("signed snapshot") # 以降のコミットに署名が付与される
|
|
294
|
+
|
|
295
|
+
result = ssm.verify() # 整合性(再ハッシュ)と署名を検証
|
|
296
|
+
# {'integrity_ok': True, 'signed': True, 'signature_ok': True, 'issues': []}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 構造化ロギング(v2.1.0)
|
|
300
|
+
|
|
301
|
+
```python
|
|
302
|
+
from SessionSmith import setup_logging, enable_debug, set_log_level
|
|
303
|
+
|
|
304
|
+
setup_logging(level="INFO", log_file="ssm.log") # ファイル出力(ローテーション付き)
|
|
305
|
+
setup_logging(level="INFO", json_format=True) # JSON 構造化ログ
|
|
306
|
+
enable_debug() # デバッグモード
|
|
307
|
+
|
|
308
|
+
# 環境変数でも設定可能:
|
|
309
|
+
# SESSIONSMITH_LOG_LEVEL=DEBUG
|
|
310
|
+
# SESSIONSMITH_LOG_FILE=ssm.log
|
|
311
|
+
# SESSIONSMITH_LOG_JSON=1
|
|
312
|
+
```
|
|
313
|
+
|
|
231
314
|
### レガシーAPI(後方互換性)
|
|
232
315
|
|
|
233
316
|
> ⚠️ 以下のAPIは後方互換性のために残されています。新規開発では `ssm` の使用を推奨します。
|
|
@@ -407,6 +490,10 @@ SessionSmithには、Cursor/VSCode用の拡張機能が用意されています
|
|
|
407
490
|
|
|
408
491
|
### 機能
|
|
409
492
|
|
|
493
|
+
- 🌳 **Session Graph(v0.2.0〜)**: `.ssm/` のコミット履歴を gitgraph 風に可視化。
|
|
494
|
+
ブランチのレーン色分け、マージの分岐・合流、ブランチ/タグ/HEADバッジ、コミット詳細
|
|
495
|
+
(変数一覧・署名状態)、GUI からの Checkout / Branch / Tag / Commit に対応
|
|
496
|
+
- 🗂 **Sessions ビュー(v0.2.0〜)**: アクティビティバーにブランチ・タグ・コミットのツリー表示
|
|
410
497
|
- ✅ **Save Session**: 現在のPythonセッション(変数)を保存
|
|
411
498
|
- ✅ **Load Session**: セッションファイルを選択して変数を復元
|
|
412
499
|
- ✅ **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.1.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",
|
|
@@ -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
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
構造化ロギング設定モジュール
|
|
3
|
+
|
|
4
|
+
SessionSmith 全体のロガー(``SessionSmith`` 名前空間)を一元的に設定します。
|
|
5
|
+
|
|
6
|
+
- コンソール/ファイルへの出力
|
|
7
|
+
- ログレベルの設定
|
|
8
|
+
- 通常形式 / JSON 形式(構造化ログ)の切り替え
|
|
9
|
+
- ローテーション(サイズベース)
|
|
10
|
+
- 環境変数による設定
|
|
11
|
+
|
|
12
|
+
環境変数:
|
|
13
|
+
SESSIONSMITH_LOG_LEVEL ログレベル(DEBUG/INFO/WARNING/ERROR/CRITICAL)
|
|
14
|
+
SESSIONSMITH_LOG_FILE ログファイルの出力先パス
|
|
15
|
+
SESSIONSMITH_LOG_JSON "1"/"true" で JSON 形式の構造化ログを有効化
|
|
16
|
+
|
|
17
|
+
使用例:
|
|
18
|
+
>>> from SessionSmith import setup_logging, enable_debug
|
|
19
|
+
>>> setup_logging(level="INFO", log_file="ssm.log")
|
|
20
|
+
>>> enable_debug() # デバッグモードを有効化
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
import json
|
|
24
|
+
import logging
|
|
25
|
+
import logging.handlers
|
|
26
|
+
import os
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Optional, Union
|
|
29
|
+
|
|
30
|
+
# SessionSmith 全体のルートロガー
|
|
31
|
+
ROOT_LOGGER_NAME = "SessionSmith"
|
|
32
|
+
|
|
33
|
+
DEFAULT_FORMAT = "%(asctime)s [%(levelname)s] %(name)s: %(message)s"
|
|
34
|
+
DEFAULT_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
|
|
35
|
+
|
|
36
|
+
# setup_logging で追加したハンドラーを識別するためのマーカー属性
|
|
37
|
+
_HANDLER_MARKER = "_sessionsmith_handler"
|
|
38
|
+
|
|
39
|
+
_configured = False
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class JsonFormatter(logging.Formatter):
|
|
43
|
+
"""ログレコードを JSON 1行で出力するフォーマッター(構造化ログ)。"""
|
|
44
|
+
|
|
45
|
+
def format(self, record: logging.LogRecord) -> str:
|
|
46
|
+
payload = {
|
|
47
|
+
"timestamp": self.formatTime(record, DEFAULT_DATE_FORMAT),
|
|
48
|
+
"level": record.levelname,
|
|
49
|
+
"logger": record.name,
|
|
50
|
+
"message": record.getMessage(),
|
|
51
|
+
}
|
|
52
|
+
if record.exc_info:
|
|
53
|
+
payload["exception"] = self.formatException(record.exc_info)
|
|
54
|
+
# 追加の文脈情報(extra=...)を含める
|
|
55
|
+
standard = set(logging.LogRecord("", 0, "", 0, "", (), None).__dict__)
|
|
56
|
+
standard.update({"message", "asctime"})
|
|
57
|
+
for key, value in record.__dict__.items():
|
|
58
|
+
if key not in standard and not key.startswith("_"):
|
|
59
|
+
payload[key] = value
|
|
60
|
+
return json.dumps(payload, ensure_ascii=False, default=str)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def _normalize_level(level: Union[int, str]) -> int:
|
|
64
|
+
"""ログレベルを int に正規化します。"""
|
|
65
|
+
if isinstance(level, int):
|
|
66
|
+
return level
|
|
67
|
+
resolved = logging.getLevelName(str(level).upper())
|
|
68
|
+
if not isinstance(resolved, int):
|
|
69
|
+
raise ValueError(f"Invalid log level: {level}")
|
|
70
|
+
return resolved
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _remove_managed_handlers(logger: logging.Logger) -> None:
|
|
74
|
+
"""setup_logging が以前に追加したハンドラーを取り除きます(多重設定防止)。"""
|
|
75
|
+
for handler in list(logger.handlers):
|
|
76
|
+
if getattr(handler, _HANDLER_MARKER, False):
|
|
77
|
+
logger.removeHandler(handler)
|
|
78
|
+
try:
|
|
79
|
+
handler.close()
|
|
80
|
+
except Exception:
|
|
81
|
+
pass
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def setup_logging(
|
|
85
|
+
level: Union[int, str] = "INFO",
|
|
86
|
+
log_file: Optional[Union[str, Path]] = None,
|
|
87
|
+
*,
|
|
88
|
+
console: bool = True,
|
|
89
|
+
json_format: bool = False,
|
|
90
|
+
fmt: str = DEFAULT_FORMAT,
|
|
91
|
+
max_bytes: int = 5 * 1024 * 1024,
|
|
92
|
+
backup_count: int = 3,
|
|
93
|
+
) -> logging.Logger:
|
|
94
|
+
"""
|
|
95
|
+
SessionSmith のロギングを設定します。
|
|
96
|
+
|
|
97
|
+
Args:
|
|
98
|
+
level: ログレベル("DEBUG" 等の文字列または ``logging.DEBUG`` 等の int)
|
|
99
|
+
log_file: ログファイルの出力先(None でファイル出力なし)
|
|
100
|
+
console: コンソール(stderr)へ出力するか
|
|
101
|
+
json_format: JSON 形式の構造化ログを使用するか
|
|
102
|
+
fmt: 通常形式のフォーマット文字列
|
|
103
|
+
max_bytes: ログファイルのローテーション閾値(バイト)
|
|
104
|
+
backup_count: 保持するローテーションファイル数
|
|
105
|
+
|
|
106
|
+
Returns:
|
|
107
|
+
logging.Logger: 設定済みの SessionSmith ルートロガー
|
|
108
|
+
"""
|
|
109
|
+
global _configured
|
|
110
|
+
|
|
111
|
+
logger = logging.getLogger(ROOT_LOGGER_NAME)
|
|
112
|
+
logger.setLevel(_normalize_level(level))
|
|
113
|
+
# ルートロガーへの伝播を止め、重複出力を防ぐ
|
|
114
|
+
logger.propagate = False
|
|
115
|
+
|
|
116
|
+
_remove_managed_handlers(logger)
|
|
117
|
+
|
|
118
|
+
formatter: logging.Formatter = JsonFormatter() if json_format else logging.Formatter(
|
|
119
|
+
fmt, datefmt=DEFAULT_DATE_FORMAT
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
if console:
|
|
123
|
+
stream_handler = logging.StreamHandler()
|
|
124
|
+
stream_handler.setFormatter(formatter)
|
|
125
|
+
setattr(stream_handler, _HANDLER_MARKER, True)
|
|
126
|
+
logger.addHandler(stream_handler)
|
|
127
|
+
|
|
128
|
+
if log_file is not None:
|
|
129
|
+
log_path = Path(log_file)
|
|
130
|
+
if log_path.parent and not log_path.parent.exists():
|
|
131
|
+
log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
file_handler = logging.handlers.RotatingFileHandler(
|
|
133
|
+
str(log_path),
|
|
134
|
+
maxBytes=max_bytes,
|
|
135
|
+
backupCount=backup_count,
|
|
136
|
+
encoding="utf-8",
|
|
137
|
+
)
|
|
138
|
+
file_handler.setFormatter(formatter)
|
|
139
|
+
setattr(file_handler, _HANDLER_MARKER, True)
|
|
140
|
+
logger.addHandler(file_handler)
|
|
141
|
+
|
|
142
|
+
# ハンドラーが何も無い場合は NullHandler を入れて警告を抑制
|
|
143
|
+
if not logger.handlers:
|
|
144
|
+
logger.addHandler(logging.NullHandler())
|
|
145
|
+
|
|
146
|
+
_configured = True
|
|
147
|
+
return logger
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def set_log_level(level: Union[int, str]) -> None:
|
|
151
|
+
"""SessionSmith ロガーのログレベルを変更します。"""
|
|
152
|
+
logging.getLogger(ROOT_LOGGER_NAME).setLevel(_normalize_level(level))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def get_log_level() -> str:
|
|
156
|
+
"""現在の SessionSmith ロガーのログレベル名を返します。"""
|
|
157
|
+
return logging.getLevelName(logging.getLogger(ROOT_LOGGER_NAME).level)
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
def enable_debug(log_file: Optional[Union[str, Path]] = None) -> logging.Logger:
|
|
161
|
+
"""
|
|
162
|
+
デバッグモードを有効化します(DEBUG レベル + コンソール出力)。
|
|
163
|
+
|
|
164
|
+
Args:
|
|
165
|
+
log_file: 併せてファイル出力する場合のパス
|
|
166
|
+
|
|
167
|
+
Returns:
|
|
168
|
+
logging.Logger: 設定済みのロガー
|
|
169
|
+
"""
|
|
170
|
+
return setup_logging(level="DEBUG", log_file=log_file, console=True)
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def configure_from_env() -> Optional[logging.Logger]:
|
|
174
|
+
"""
|
|
175
|
+
環境変数からロギングを設定します。
|
|
176
|
+
|
|
177
|
+
``SESSIONSMITH_LOG_LEVEL`` または ``SESSIONSMITH_LOG_FILE`` のいずれかが
|
|
178
|
+
設定されている場合のみ設定を行います。
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
logging.Logger or None: 設定した場合はロガー、未設定なら None
|
|
182
|
+
"""
|
|
183
|
+
level = os.environ.get("SESSIONSMITH_LOG_LEVEL")
|
|
184
|
+
log_file = os.environ.get("SESSIONSMITH_LOG_FILE")
|
|
185
|
+
json_format = os.environ.get("SESSIONSMITH_LOG_JSON", "").lower() in ("1", "true", "yes")
|
|
186
|
+
|
|
187
|
+
if not level and not log_file:
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
return setup_logging(
|
|
191
|
+
level=level or "INFO",
|
|
192
|
+
log_file=log_file,
|
|
193
|
+
json_format=json_format,
|
|
194
|
+
)
|