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.
Files changed (43) hide show
  1. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/PKG-INFO +88 -1
  2. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/__init__.py +40 -1
  3. sessionsmith-2.1.0/SessionSmith/crypto.py +205 -0
  4. sessionsmith-2.1.0/SessionSmith/logging_config.py +194 -0
  5. sessionsmith-2.1.0/SessionSmith/remote_backends.py +277 -0
  6. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/ssm.py +362 -32
  7. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/PKG-INFO +88 -1
  8. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/SOURCES.txt +8 -1
  9. sessionsmith-2.1.0/SessionSmith.egg-info/requires.txt +32 -0
  10. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/pyproject.toml +10 -0
  11. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/readme.md +74 -0
  12. sessionsmith-2.1.0/tests/test_crypto.py +95 -0
  13. sessionsmith-2.1.0/tests/test_logging_config.py +106 -0
  14. sessionsmith-2.1.0/tests/test_remote_backends.py +166 -0
  15. sessionsmith-2.1.0/tests/test_verify.py +66 -0
  16. sessionsmith-2.0.0/SessionSmith.egg-info/requires.txt +0 -15
  17. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/LICENSE +0 -0
  18. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/cli.py +0 -0
  19. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/compare.py +0 -0
  20. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/core.py +0 -0
  21. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/error_handling.py +0 -0
  22. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/exceptions.py +0 -0
  23. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/formats.py +0 -0
  24. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/i18n.py +0 -0
  25. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/info.py +0 -0
  26. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/jupyter_utils.py +0 -0
  27. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/manager.py +0 -0
  28. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/py.typed +0 -0
  29. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/resource_manager.py +0 -0
  30. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/serializers.py +0 -0
  31. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/tracer.py +0 -0
  32. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/utils.py +0 -0
  33. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/visualizer.py +0 -0
  34. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/visualizer_arrays.py +0 -0
  35. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith/visualizer_generic.py +0 -0
  36. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/dependency_links.txt +0 -0
  37. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/entry_points.txt +0 -0
  38. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/SessionSmith.egg-info/top_level.txt +0 -0
  39. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/setup.cfg +0 -0
  40. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/setup.py +0 -0
  41. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/tests/test_cli.py +0 -0
  42. {sessionsmith-2.0.0 → sessionsmith-2.1.0}/tests/test_core.py +0 -0
  43. {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.0.0
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.0.0"
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
+ )