yee88 0.3.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.
- yee88/__init__.py +1 -0
- yee88/api.py +116 -0
- yee88/backends.py +25 -0
- yee88/backends_helpers.py +14 -0
- yee88/cli/__init__.py +228 -0
- yee88/cli/config.py +320 -0
- yee88/cli/doctor.py +173 -0
- yee88/cli/init.py +113 -0
- yee88/cli/onboarding_cmd.py +126 -0
- yee88/cli/plugins.py +196 -0
- yee88/cli/run.py +419 -0
- yee88/cli/topic.py +355 -0
- yee88/commands.py +134 -0
- yee88/config.py +142 -0
- yee88/config_migrations.py +124 -0
- yee88/config_watch.py +146 -0
- yee88/context.py +9 -0
- yee88/directives.py +146 -0
- yee88/engines.py +53 -0
- yee88/events.py +170 -0
- yee88/ids.py +17 -0
- yee88/lockfile.py +158 -0
- yee88/logging.py +283 -0
- yee88/markdown.py +298 -0
- yee88/model.py +77 -0
- yee88/plugins.py +312 -0
- yee88/presenter.py +25 -0
- yee88/progress.py +99 -0
- yee88/router.py +113 -0
- yee88/runner.py +712 -0
- yee88/runner_bridge.py +619 -0
- yee88/runners/__init__.py +1 -0
- yee88/runners/claude.py +483 -0
- yee88/runners/codex.py +656 -0
- yee88/runners/mock.py +221 -0
- yee88/runners/opencode.py +505 -0
- yee88/runners/pi.py +523 -0
- yee88/runners/run_options.py +39 -0
- yee88/runners/tool_actions.py +90 -0
- yee88/runtime_loader.py +207 -0
- yee88/scheduler.py +159 -0
- yee88/schemas/__init__.py +1 -0
- yee88/schemas/claude.py +238 -0
- yee88/schemas/codex.py +169 -0
- yee88/schemas/opencode.py +51 -0
- yee88/schemas/pi.py +117 -0
- yee88/settings.py +360 -0
- yee88/telegram/__init__.py +20 -0
- yee88/telegram/api_models.py +37 -0
- yee88/telegram/api_schemas.py +152 -0
- yee88/telegram/backend.py +163 -0
- yee88/telegram/bridge.py +425 -0
- yee88/telegram/chat_prefs.py +242 -0
- yee88/telegram/chat_sessions.py +112 -0
- yee88/telegram/client.py +409 -0
- yee88/telegram/client_api.py +539 -0
- yee88/telegram/commands/__init__.py +12 -0
- yee88/telegram/commands/agent.py +196 -0
- yee88/telegram/commands/cancel.py +116 -0
- yee88/telegram/commands/dispatch.py +111 -0
- yee88/telegram/commands/executor.py +449 -0
- yee88/telegram/commands/file_transfer.py +586 -0
- yee88/telegram/commands/handlers.py +45 -0
- yee88/telegram/commands/media.py +143 -0
- yee88/telegram/commands/menu.py +139 -0
- yee88/telegram/commands/model.py +215 -0
- yee88/telegram/commands/overrides.py +159 -0
- yee88/telegram/commands/parse.py +30 -0
- yee88/telegram/commands/plan.py +16 -0
- yee88/telegram/commands/reasoning.py +234 -0
- yee88/telegram/commands/reply.py +23 -0
- yee88/telegram/commands/topics.py +332 -0
- yee88/telegram/commands/trigger.py +143 -0
- yee88/telegram/context.py +140 -0
- yee88/telegram/engine_defaults.py +86 -0
- yee88/telegram/engine_overrides.py +105 -0
- yee88/telegram/files.py +178 -0
- yee88/telegram/loop.py +1822 -0
- yee88/telegram/onboarding.py +1088 -0
- yee88/telegram/outbox.py +177 -0
- yee88/telegram/parsing.py +239 -0
- yee88/telegram/render.py +198 -0
- yee88/telegram/state_store.py +88 -0
- yee88/telegram/topic_state.py +334 -0
- yee88/telegram/topics.py +256 -0
- yee88/telegram/trigger_mode.py +68 -0
- yee88/telegram/types.py +63 -0
- yee88/telegram/voice.py +110 -0
- yee88/transport.py +53 -0
- yee88/transport_runtime.py +323 -0
- yee88/transports.py +76 -0
- yee88/utils/__init__.py +1 -0
- yee88/utils/git.py +87 -0
- yee88/utils/json_state.py +21 -0
- yee88/utils/paths.py +47 -0
- yee88/utils/streams.py +44 -0
- yee88/utils/subprocess.py +86 -0
- yee88/worktrees.py +135 -0
- yee88-0.3.0.dist-info/METADATA +116 -0
- yee88-0.3.0.dist-info/RECORD +103 -0
- yee88-0.3.0.dist-info/WHEEL +4 -0
- yee88-0.3.0.dist-info/entry_points.txt +11 -0
- yee88-0.3.0.dist-info/licenses/LICENSE +21 -0
yee88/lockfile.py
ADDED
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import hashlib
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
|
|
9
|
+
from .logging import get_logger
|
|
10
|
+
|
|
11
|
+
logger = get_logger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True, slots=True)
|
|
15
|
+
class LockInfo:
|
|
16
|
+
pid: int | None
|
|
17
|
+
token_fingerprint: str | None
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class LockError(RuntimeError):
|
|
21
|
+
def __init__(
|
|
22
|
+
self,
|
|
23
|
+
*,
|
|
24
|
+
path: Path,
|
|
25
|
+
state: str,
|
|
26
|
+
) -> None:
|
|
27
|
+
self.path = path
|
|
28
|
+
self.state = state
|
|
29
|
+
super().__init__(_format_lock_message(path, state))
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@dataclass(slots=True)
|
|
33
|
+
class LockHandle:
|
|
34
|
+
path: Path
|
|
35
|
+
|
|
36
|
+
def release(self) -> None:
|
|
37
|
+
try:
|
|
38
|
+
self.path.unlink(missing_ok=True)
|
|
39
|
+
except OSError as exc:
|
|
40
|
+
logger.warning(
|
|
41
|
+
"lock.release.failed",
|
|
42
|
+
path=str(self.path),
|
|
43
|
+
error=str(exc),
|
|
44
|
+
error_type=exc.__class__.__name__,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
def __enter__(self) -> LockHandle:
|
|
48
|
+
return self
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
51
|
+
self.release()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def token_fingerprint(token: str) -> str:
|
|
55
|
+
digest = hashlib.sha256(token.encode("utf-8")).hexdigest()
|
|
56
|
+
return digest[:10]
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def lock_path_for_config(config_path: Path) -> Path:
|
|
60
|
+
return config_path.with_suffix(".lock")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def acquire_lock(
|
|
64
|
+
*, config_path: Path, token_fingerprint: str | None = None
|
|
65
|
+
) -> LockHandle:
|
|
66
|
+
cfg_path = config_path.expanduser().resolve()
|
|
67
|
+
lock_path = lock_path_for_config(cfg_path)
|
|
68
|
+
try:
|
|
69
|
+
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
70
|
+
existing = _read_lock_info(lock_path)
|
|
71
|
+
if existing:
|
|
72
|
+
if (
|
|
73
|
+
token_fingerprint
|
|
74
|
+
and existing.token_fingerprint
|
|
75
|
+
and existing.token_fingerprint != token_fingerprint
|
|
76
|
+
):
|
|
77
|
+
_write_lock_info(
|
|
78
|
+
lock_path,
|
|
79
|
+
pid=os.getpid(),
|
|
80
|
+
token_fingerprint=token_fingerprint,
|
|
81
|
+
)
|
|
82
|
+
return LockHandle(path=lock_path)
|
|
83
|
+
if _pid_running(existing.pid):
|
|
84
|
+
raise LockError(path=lock_path, state="running") from None
|
|
85
|
+
_write_lock_info(
|
|
86
|
+
lock_path,
|
|
87
|
+
pid=os.getpid(),
|
|
88
|
+
token_fingerprint=token_fingerprint,
|
|
89
|
+
)
|
|
90
|
+
except OSError as exc:
|
|
91
|
+
raise LockError(path=lock_path, state=str(exc)) from exc
|
|
92
|
+
|
|
93
|
+
return LockHandle(path=lock_path)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _read_lock_info(path: Path) -> LockInfo | None:
|
|
97
|
+
try:
|
|
98
|
+
raw = path.read_text(encoding="utf-8")
|
|
99
|
+
except FileNotFoundError:
|
|
100
|
+
return None
|
|
101
|
+
except OSError:
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
data = json.loads(raw)
|
|
105
|
+
except json.JSONDecodeError:
|
|
106
|
+
return None
|
|
107
|
+
if not isinstance(data, dict):
|
|
108
|
+
return None
|
|
109
|
+
pid = data.get("pid")
|
|
110
|
+
if isinstance(pid, bool) or not isinstance(pid, int):
|
|
111
|
+
pid = None
|
|
112
|
+
token_hint = data.get("token_fingerprint")
|
|
113
|
+
if not isinstance(token_hint, str):
|
|
114
|
+
token_hint = None
|
|
115
|
+
return LockInfo(
|
|
116
|
+
pid=pid,
|
|
117
|
+
token_fingerprint=token_hint,
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _write_lock_info(path: Path, *, pid: int, token_fingerprint: str | None) -> None:
|
|
122
|
+
payload = {"pid": pid, "token_fingerprint": token_fingerprint}
|
|
123
|
+
path.write_text(
|
|
124
|
+
json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def _pid_running(pid: int | None) -> bool:
|
|
129
|
+
if pid is None or pid <= 0:
|
|
130
|
+
return False
|
|
131
|
+
try:
|
|
132
|
+
os.kill(pid, 0)
|
|
133
|
+
except ProcessLookupError:
|
|
134
|
+
return False
|
|
135
|
+
except PermissionError:
|
|
136
|
+
return True
|
|
137
|
+
except OSError:
|
|
138
|
+
return False
|
|
139
|
+
return True
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _format_lock_message(path: Path, state: str) -> str:
|
|
143
|
+
if state != "running":
|
|
144
|
+
return f"error: lock failed: {state}"
|
|
145
|
+
header = "error: already running"
|
|
146
|
+
display_path = _display_lock_path(path)
|
|
147
|
+
lines = [header, f"remove {display_path} if stale"]
|
|
148
|
+
return "\n".join(lines)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _display_lock_path(path: Path) -> str:
|
|
152
|
+
home = Path.home()
|
|
153
|
+
try:
|
|
154
|
+
resolved = path.expanduser().resolve()
|
|
155
|
+
rel = resolved.relative_to(home)
|
|
156
|
+
return f"~/{rel}"
|
|
157
|
+
except (ValueError, OSError):
|
|
158
|
+
return str(path)
|
yee88/logging.py
ADDED
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import errno
|
|
4
|
+
import io
|
|
5
|
+
import os
|
|
6
|
+
import re
|
|
7
|
+
import sys
|
|
8
|
+
from contextlib import contextmanager
|
|
9
|
+
from contextvars import ContextVar
|
|
10
|
+
from typing import Any, TextIO, cast
|
|
11
|
+
|
|
12
|
+
import structlog
|
|
13
|
+
from structlog.types import Processor
|
|
14
|
+
|
|
15
|
+
TELEGRAM_TOKEN_RE = re.compile(r"bot\d+:[A-Za-z0-9_-]+")
|
|
16
|
+
TELEGRAM_BARE_TOKEN_RE = re.compile(r"\b\d+:[A-Za-z0-9_-]{10,}\b")
|
|
17
|
+
|
|
18
|
+
_LEVELS: dict[str, int] = {
|
|
19
|
+
"debug": 10,
|
|
20
|
+
"info": 20,
|
|
21
|
+
"warning": 30,
|
|
22
|
+
"error": 40,
|
|
23
|
+
"exception": 40,
|
|
24
|
+
"critical": 50,
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
_MIN_LEVEL = _LEVELS["info"]
|
|
28
|
+
_PIPELINE_LEVEL_NAME = "debug"
|
|
29
|
+
|
|
30
|
+
_suppress_below: ContextVar[int | None] = ContextVar(
|
|
31
|
+
"yee88_suppress_below", default=None
|
|
32
|
+
)
|
|
33
|
+
_log_file_handle: TextIO | None = None
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _truthy(value: str | None) -> bool:
|
|
37
|
+
if value is None:
|
|
38
|
+
return False
|
|
39
|
+
return value.strip().lower() in {"1", "true", "yes", "on"}
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _level_value(value: str | None, *, default: str = "info") -> int:
|
|
43
|
+
if not value:
|
|
44
|
+
return _LEVELS[default]
|
|
45
|
+
level = _LEVELS.get(value.strip().lower())
|
|
46
|
+
return level if level is not None else _LEVELS[default]
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def pipeline_log_level() -> str:
|
|
50
|
+
return _PIPELINE_LEVEL_NAME
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def log_pipeline(logger: Any, event: str, **fields: Any) -> None:
|
|
54
|
+
if _PIPELINE_LEVEL_NAME == "info":
|
|
55
|
+
logger.info(event, **fields)
|
|
56
|
+
else:
|
|
57
|
+
logger.debug(event, **fields)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _drop_below_level(
|
|
61
|
+
logger: Any, method_name: str, event_dict: dict[str, Any]
|
|
62
|
+
) -> dict[str, Any]:
|
|
63
|
+
level_value = _LEVELS.get(method_name, 0)
|
|
64
|
+
if level_value < _MIN_LEVEL:
|
|
65
|
+
raise structlog.DropEvent
|
|
66
|
+
suppress = _suppress_below.get()
|
|
67
|
+
if suppress is not None and level_value < suppress:
|
|
68
|
+
raise structlog.DropEvent
|
|
69
|
+
return event_dict
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _redact_text(value: str) -> str:
|
|
73
|
+
redacted = TELEGRAM_TOKEN_RE.sub("bot[REDACTED]", value)
|
|
74
|
+
return TELEGRAM_BARE_TOKEN_RE.sub("[REDACTED_TOKEN]", redacted)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _redact_value(value: Any, memo: dict[int, Any]) -> Any:
|
|
78
|
+
if isinstance(value, str):
|
|
79
|
+
return _redact_text(value)
|
|
80
|
+
if isinstance(value, (bytes, bytearray)):
|
|
81
|
+
return _redact_text(value.decode("utf-8", errors="replace"))
|
|
82
|
+
obj_id = id(value)
|
|
83
|
+
if obj_id in memo:
|
|
84
|
+
return memo[obj_id]
|
|
85
|
+
if isinstance(value, dict):
|
|
86
|
+
redacted: dict[Any, Any] = {}
|
|
87
|
+
memo[obj_id] = redacted
|
|
88
|
+
for key, val in value.items():
|
|
89
|
+
redacted[key] = _redact_value(val, memo)
|
|
90
|
+
return redacted
|
|
91
|
+
if isinstance(value, list):
|
|
92
|
+
redacted_list: list[Any] = []
|
|
93
|
+
memo[obj_id] = redacted_list
|
|
94
|
+
redacted_list.extend(_redact_value(item, memo) for item in value)
|
|
95
|
+
return redacted_list
|
|
96
|
+
if isinstance(value, tuple):
|
|
97
|
+
redacted_tuple: list[Any] = []
|
|
98
|
+
memo[obj_id] = redacted_tuple
|
|
99
|
+
redacted_tuple.extend(_redact_value(item, memo) for item in value)
|
|
100
|
+
return tuple(redacted_tuple)
|
|
101
|
+
if isinstance(value, set):
|
|
102
|
+
redacted_set: set[Any] = set()
|
|
103
|
+
memo[obj_id] = redacted_set
|
|
104
|
+
redacted_set.update(_redact_value(item, memo) for item in value)
|
|
105
|
+
return redacted_set
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _redact_event_dict(
|
|
110
|
+
_logger: Any, _method_name: str, event_dict: dict[str, Any]
|
|
111
|
+
) -> dict[str, Any]:
|
|
112
|
+
return _redact_value(event_dict, memo={})
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _file_sink(
|
|
116
|
+
logger: Any, method_name: str, event_dict: dict[str, Any]
|
|
117
|
+
) -> dict[str, Any]:
|
|
118
|
+
if _log_file_handle is None:
|
|
119
|
+
return event_dict
|
|
120
|
+
try:
|
|
121
|
+
payload = structlog.processors.JSONRenderer(default=str)(
|
|
122
|
+
logger, method_name, dict(event_dict)
|
|
123
|
+
)
|
|
124
|
+
if isinstance(payload, bytes):
|
|
125
|
+
payload = payload.decode("utf-8", errors="replace")
|
|
126
|
+
_log_file_handle.write(payload + "\n")
|
|
127
|
+
_log_file_handle.flush()
|
|
128
|
+
except Exception: # noqa: BLE001
|
|
129
|
+
return event_dict
|
|
130
|
+
return event_dict
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _add_logger_name(
|
|
134
|
+
logger: Any, method_name: str, event_dict: dict[str, Any]
|
|
135
|
+
) -> dict[str, Any]:
|
|
136
|
+
if "logger" in event_dict:
|
|
137
|
+
return event_dict
|
|
138
|
+
name = event_dict.pop("logger_name", None)
|
|
139
|
+
if isinstance(name, str) and name:
|
|
140
|
+
event_dict["logger"] = name
|
|
141
|
+
return event_dict
|
|
142
|
+
fallback = getattr(logger, "name", None)
|
|
143
|
+
if isinstance(fallback, str) and fallback:
|
|
144
|
+
event_dict["logger"] = fallback
|
|
145
|
+
return event_dict
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def get_logger(name: str | None = None) -> Any:
|
|
149
|
+
if name:
|
|
150
|
+
return structlog.get_logger(logger_name=name)
|
|
151
|
+
return structlog.get_logger()
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def bind_run_context(**fields: Any) -> None:
|
|
155
|
+
structlog.contextvars.bind_contextvars(**fields)
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def clear_context() -> None:
|
|
159
|
+
structlog.contextvars.clear_contextvars()
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
class SafeWriter(io.TextIOBase):
|
|
163
|
+
def __init__(self, stream: Any) -> None:
|
|
164
|
+
self._stream = stream
|
|
165
|
+
self._closed = False
|
|
166
|
+
|
|
167
|
+
def write(self, message: str) -> int:
|
|
168
|
+
if self._closed:
|
|
169
|
+
return 0
|
|
170
|
+
try:
|
|
171
|
+
return self._stream.write(message)
|
|
172
|
+
except (BrokenPipeError, ValueError):
|
|
173
|
+
self._close()
|
|
174
|
+
return 0
|
|
175
|
+
except OSError as exc:
|
|
176
|
+
if exc.errno == errno.EPIPE:
|
|
177
|
+
self._close()
|
|
178
|
+
return 0
|
|
179
|
+
raise
|
|
180
|
+
|
|
181
|
+
def flush(self) -> None:
|
|
182
|
+
if self._closed:
|
|
183
|
+
return
|
|
184
|
+
try:
|
|
185
|
+
self._stream.flush()
|
|
186
|
+
except (BrokenPipeError, ValueError):
|
|
187
|
+
self._close()
|
|
188
|
+
except OSError as exc:
|
|
189
|
+
if exc.errno == errno.EPIPE:
|
|
190
|
+
self._close()
|
|
191
|
+
return
|
|
192
|
+
raise
|
|
193
|
+
|
|
194
|
+
def isatty(self) -> bool:
|
|
195
|
+
isatty = getattr(self._stream, "isatty", None)
|
|
196
|
+
return bool(isatty()) if callable(isatty) else False
|
|
197
|
+
|
|
198
|
+
def _close(self) -> None:
|
|
199
|
+
if self._closed:
|
|
200
|
+
return
|
|
201
|
+
self._closed = True
|
|
202
|
+
try:
|
|
203
|
+
self._stream.close()
|
|
204
|
+
except Exception: # noqa: BLE001
|
|
205
|
+
return
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def setup_logging(
|
|
209
|
+
*, debug: bool = False, cache_logger_on_first_use: bool = False
|
|
210
|
+
) -> None:
|
|
211
|
+
global _MIN_LEVEL, _PIPELINE_LEVEL_NAME
|
|
212
|
+
global _log_file_handle
|
|
213
|
+
|
|
214
|
+
level_name = os.environ.get("TAKOPI_LOG_LEVEL")
|
|
215
|
+
if debug:
|
|
216
|
+
level_name = "debug"
|
|
217
|
+
_MIN_LEVEL = _level_value(level_name, default="info")
|
|
218
|
+
|
|
219
|
+
trace_pipeline = _truthy(os.environ.get("TAKOPI_TRACE_PIPELINE"))
|
|
220
|
+
_PIPELINE_LEVEL_NAME = "info" if trace_pipeline else "debug"
|
|
221
|
+
|
|
222
|
+
format_value = os.environ.get("TAKOPI_LOG_FORMAT", "console").strip().lower()
|
|
223
|
+
color_override = os.environ.get("TAKOPI_LOG_COLOR")
|
|
224
|
+
is_tty = sys.stdout.isatty() if color_override is None else _truthy(color_override)
|
|
225
|
+
if format_value == "json":
|
|
226
|
+
renderer: Any = structlog.processors.JSONRenderer(default=str)
|
|
227
|
+
else:
|
|
228
|
+
renderer = structlog.dev.ConsoleRenderer(colors=is_tty)
|
|
229
|
+
|
|
230
|
+
safe_stream = cast(TextIO, SafeWriter(sys.stdout))
|
|
231
|
+
log_file = os.environ.get("TAKOPI_LOG_FILE")
|
|
232
|
+
if _log_file_handle is not None:
|
|
233
|
+
try:
|
|
234
|
+
_log_file_handle.close()
|
|
235
|
+
except Exception: # noqa: BLE001
|
|
236
|
+
_log_file_handle = None
|
|
237
|
+
else:
|
|
238
|
+
_log_file_handle = None
|
|
239
|
+
if log_file:
|
|
240
|
+
try:
|
|
241
|
+
_log_file_handle = open( # noqa: SIM115
|
|
242
|
+
log_file, "a", encoding="utf-8"
|
|
243
|
+
)
|
|
244
|
+
except OSError:
|
|
245
|
+
_log_file_handle = None
|
|
246
|
+
|
|
247
|
+
processors = cast(
|
|
248
|
+
list[Processor],
|
|
249
|
+
[
|
|
250
|
+
_drop_below_level,
|
|
251
|
+
structlog.contextvars.merge_contextvars,
|
|
252
|
+
structlog.processors.TimeStamper(fmt="iso", utc=True),
|
|
253
|
+
structlog.processors.add_log_level,
|
|
254
|
+
_add_logger_name,
|
|
255
|
+
],
|
|
256
|
+
)
|
|
257
|
+
if format_value == "json":
|
|
258
|
+
processors.append(structlog.processors.format_exc_info)
|
|
259
|
+
processors.extend(
|
|
260
|
+
cast(
|
|
261
|
+
list[Processor],
|
|
262
|
+
[
|
|
263
|
+
_redact_event_dict,
|
|
264
|
+
_file_sink,
|
|
265
|
+
cast(Processor, renderer),
|
|
266
|
+
],
|
|
267
|
+
)
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
structlog.configure(
|
|
271
|
+
processors=processors,
|
|
272
|
+
logger_factory=structlog.PrintLoggerFactory(file=safe_stream),
|
|
273
|
+
cache_logger_on_first_use=cache_logger_on_first_use,
|
|
274
|
+
)
|
|
275
|
+
|
|
276
|
+
|
|
277
|
+
@contextmanager
|
|
278
|
+
def suppress_logs(level: str = "warning"):
|
|
279
|
+
token = _suppress_below.set(_level_value(level, default="warning"))
|
|
280
|
+
try:
|
|
281
|
+
yield
|
|
282
|
+
finally:
|
|
283
|
+
_suppress_below.reset(token)
|