code-analysis-client-dev 1.6.341__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.
- code_analysis_client/__init__.py +142 -0
- code_analysis_client/check_chain.py +374 -0
- code_analysis_client/client.py +728 -0
- code_analysis_client/commands_proxy.py +63 -0
- code_analysis_client/config.py +198 -0
- code_analysis_client/exceptions.py +188 -0
- code_analysis_client/file_session.py +823 -0
- code_analysis_client/project.py +59 -0
- code_analysis_client/py.typed +0 -0
- code_analysis_client/queue_wait.py +341 -0
- code_analysis_client/responses.py +126 -0
- code_analysis_client/schema_disk_cache.py +207 -0
- code_analysis_client/server_api.py +291 -0
- code_analysis_client/server_schema.py +57 -0
- code_analysis_client/symbol_tree.py +13 -0
- code_analysis_client/universal_file.py +50 -0
- code_analysis_client/validation.py +98 -0
- code_analysis_client/vectorizer_state.py +159 -0
- code_analysis_client/version.txt +1 -0
- code_analysis_client/worktree.py +533 -0
- code_analysis_client_dev-1.6.341.dist-info/METADATA +251 -0
- code_analysis_client_dev-1.6.341.dist-info/RECORD +24 -0
- code_analysis_client_dev-1.6.341.dist-info/WHEEL +5 -0
- code_analysis_client_dev-1.6.341.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Dynamic command wrappers: schema from server ``help``, then local shallow validation.
|
|
3
|
+
|
|
4
|
+
Author: Vasiliy Zdanovskiy
|
|
5
|
+
email: vasilyvz@gmail.com
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from typing import TYPE_CHECKING, Any, Dict, Optional, cast
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from code_analysis_client.client import CodeAnalysisAsyncClient
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class ValidatedCommandsProxy:
|
|
17
|
+
"""``client.commands.<name>(**params)`` — fetch schema via ``help``, validate, ``execute_command``."""
|
|
18
|
+
|
|
19
|
+
__slots__ = ("_client",)
|
|
20
|
+
|
|
21
|
+
def __init__(self, client: CodeAnalysisAsyncClient) -> None:
|
|
22
|
+
"""Store the client used for schema lookup and validated command calls."""
|
|
23
|
+
object.__setattr__(self, "_client", client)
|
|
24
|
+
|
|
25
|
+
def clear_schema_cache(self) -> None:
|
|
26
|
+
"""Forget cached schemas (e.g. after server ``reload``)."""
|
|
27
|
+
self._client.clear_command_schema_cache()
|
|
28
|
+
|
|
29
|
+
async def fetch_schema(
|
|
30
|
+
self, command: str, *, refresh: bool = False
|
|
31
|
+
) -> Dict[str, Any]:
|
|
32
|
+
"""Return input JSON schema for ``command`` (from server ``help``)."""
|
|
33
|
+
return cast(
|
|
34
|
+
Dict[str, Any],
|
|
35
|
+
await self._client.get_command_schema(command, refresh=refresh),
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
async def invoke(
|
|
39
|
+
self,
|
|
40
|
+
command: str,
|
|
41
|
+
params: Optional[Dict[str, Any]] = None,
|
|
42
|
+
**kwargs: Any,
|
|
43
|
+
) -> Dict[str, Any]:
|
|
44
|
+
"""Merge ``params`` and ``kwargs``, validate against server schema, execute."""
|
|
45
|
+
merged: Dict[str, Any] = dict(params or {})
|
|
46
|
+
if kwargs:
|
|
47
|
+
merged.update(kwargs)
|
|
48
|
+
return cast(Dict[str, Any], await self._client.call_validated(command, merged))
|
|
49
|
+
|
|
50
|
+
def __getattr__(self, name: str) -> Any:
|
|
51
|
+
"""Return an async validated-call wrapper for an arbitrary command name."""
|
|
52
|
+
if name.startswith("_"):
|
|
53
|
+
raise AttributeError(name)
|
|
54
|
+
|
|
55
|
+
async def _bound(**kw: Any) -> Dict[str, Any]:
|
|
56
|
+
"""Execute the dynamically selected command with validated keyword params."""
|
|
57
|
+
return cast(Dict[str, Any], await self._client.call_validated(name, kw))
|
|
58
|
+
|
|
59
|
+
_bound.__name__ = name
|
|
60
|
+
_bound.__doc__ = (
|
|
61
|
+
f"Validated call for command {name!r} (schema from server help)."
|
|
62
|
+
)
|
|
63
|
+
return _bound
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Map server / adapter-style settings dicts to JsonRpcClient constructor kwargs.
|
|
3
|
+
|
|
4
|
+
Author: Vasiliy Zdanovskiy
|
|
5
|
+
email: vasilyvz@gmail.com
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import copy
|
|
11
|
+
import json
|
|
12
|
+
import os
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any, Mapping
|
|
15
|
+
|
|
16
|
+
CANONICAL_CONFIG_PATH = Path("/etc/casmgr/config.json")
|
|
17
|
+
|
|
18
|
+
#: Explicit adapter wait/timeout configuration of the client (owner decision 2026-10-24, item M5).
|
|
19
|
+
#: The client hands it to ``JsonRpcClient(ws_config=...)``; the adapter resolves every wait timeout
|
|
20
|
+
#: per call -> this config -> its own constants (60 s ``job_wait_timeout`` and ``cancel`` policy),
|
|
21
|
+
#: so nothing here is left to the adapter's defaults. ``job_wait_timeout`` 0 means no total bound
|
|
22
|
+
#: (a long queued command is never cut off); ``on_timeout`` ``detach`` means a caller timeout
|
|
23
|
+
#: leaves the job running. ``job_push`` mirrors the server section of the same name.
|
|
24
|
+
#: Keep equal to ``code_analysis.core.adapter_timeout_config`` (the server copy, same values).
|
|
25
|
+
DEFAULT_CLIENT_ADAPTER_CONFIG: dict[str, dict[str, Any]] = {
|
|
26
|
+
"websocket": {
|
|
27
|
+
"job_wait_timeout": 0,
|
|
28
|
+
"on_timeout": "detach",
|
|
29
|
+
"receive_timeout": 60,
|
|
30
|
+
"receive_timeout_buffer": 5,
|
|
31
|
+
"heartbeat_interval": 30,
|
|
32
|
+
"terminal_poll_interval": 2,
|
|
33
|
+
"close_timeout": 5,
|
|
34
|
+
"subscribe_settle_delay": 0.5,
|
|
35
|
+
"cancel_grace_seconds": 0.5,
|
|
36
|
+
},
|
|
37
|
+
"job_push": {
|
|
38
|
+
"on_caller_loss": "detach",
|
|
39
|
+
"poll_interval": 0.1,
|
|
40
|
+
},
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def merged_client_ws_config(user_config: Any = None) -> dict[str, Any]:
|
|
45
|
+
"""Return ``ws_config`` for ``JsonRpcClient``: the defaults with ``user_config`` merged over them.
|
|
46
|
+
|
|
47
|
+
``user_config`` is a mapping (or ``None``); each section is merged key by key so a partial
|
|
48
|
+
override keeps every other explicit default. Sections other than the defaults' are kept as given.
|
|
49
|
+
"""
|
|
50
|
+
merged: dict[str, Any] = copy.deepcopy(DEFAULT_CLIENT_ADAPTER_CONFIG)
|
|
51
|
+
if user_config is None:
|
|
52
|
+
return merged
|
|
53
|
+
raw = getattr(user_config, "raw_content", None) if not isinstance(user_config, Mapping) else user_config
|
|
54
|
+
if not isinstance(raw, Mapping):
|
|
55
|
+
return merged
|
|
56
|
+
for name, value in raw.items():
|
|
57
|
+
if isinstance(value, Mapping) and isinstance(merged.get(name), dict):
|
|
58
|
+
merged[name].update(value)
|
|
59
|
+
else:
|
|
60
|
+
merged[name] = copy.deepcopy(value)
|
|
61
|
+
return merged
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def resolve_config_path(explicit: str | Path | None = None) -> Path:
|
|
65
|
+
"""Return the server config path: explicit, then ``CASMGR_CONFIG``, then ``/etc/casmgr/config.json``.
|
|
66
|
+
|
|
67
|
+
Never falls back to the current working directory.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
FileNotFoundError: when no explicit path, no ``CASMGR_CONFIG`` and no ``/etc/casmgr/config.json``.
|
|
71
|
+
"""
|
|
72
|
+
if explicit is not None and str(explicit).strip():
|
|
73
|
+
return Path(str(explicit)).expanduser().resolve()
|
|
74
|
+
env_value = os.environ.get("CASMGR_CONFIG", "").strip()
|
|
75
|
+
if env_value:
|
|
76
|
+
return Path(env_value).expanduser().resolve()
|
|
77
|
+
if CANONICAL_CONFIG_PATH.is_file():
|
|
78
|
+
return CANONICAL_CONFIG_PATH
|
|
79
|
+
raise FileNotFoundError(
|
|
80
|
+
"no server config found: pass a path, set CASMGR_CONFIG, or install /etc/casmgr/config.json"
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _expand_path(value: Any) -> str | None:
|
|
85
|
+
"""Expand a non-empty filesystem path value to an absolute string."""
|
|
86
|
+
if value is None or value == "":
|
|
87
|
+
return None
|
|
88
|
+
return str(Path(str(value)).expanduser().resolve())
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def _ssl_paths_from_section(ssl_section: Any) -> dict[str, str]:
|
|
92
|
+
"""Extract and normalize TLS path fields from a config SSL section."""
|
|
93
|
+
if not isinstance(ssl_section, dict):
|
|
94
|
+
return {}
|
|
95
|
+
out: dict[str, str] = {}
|
|
96
|
+
for key in ("cert", "key", "ca", "cert_path", "key_path", "ca_path"):
|
|
97
|
+
raw = ssl_section.get(key)
|
|
98
|
+
expanded = _expand_path(raw) if raw else None
|
|
99
|
+
if expanded:
|
|
100
|
+
out[key] = expanded
|
|
101
|
+
# Normalize aliases to cert/key/ca for downstream comparison / JsonRpcClient
|
|
102
|
+
if "cert" not in out and out.get("cert_path"):
|
|
103
|
+
out["cert"] = out["cert_path"]
|
|
104
|
+
if "key" not in out and out.get("key_path"):
|
|
105
|
+
out["key"] = out["key_path"]
|
|
106
|
+
if "ca" not in out and out.get("ca_path"):
|
|
107
|
+
out["ca"] = out["ca_path"]
|
|
108
|
+
return out
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _network_from_server_config(config: Mapping[str, Any]) -> dict[str, Any]:
|
|
112
|
+
"""Return host, port, and protocol settings from a server config dict."""
|
|
113
|
+
server = config.get("server", {})
|
|
114
|
+
if not isinstance(server, dict):
|
|
115
|
+
server = {}
|
|
116
|
+
host = server.get("host", "127.0.0.1")
|
|
117
|
+
if host in ("0.0.0.0", "::", "[::]"):
|
|
118
|
+
host = "127.0.0.1"
|
|
119
|
+
return {
|
|
120
|
+
"host": host,
|
|
121
|
+
"port": int(server.get("port", 15001)),
|
|
122
|
+
"protocol": server.get("protocol", "http"),
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _client_mtls_paths_from_server_config(config: Mapping[str, Any]) -> dict[str, str]:
|
|
127
|
+
"""Prefer client.ssl; fall back to server.ssl (same rules as pipeline)."""
|
|
128
|
+
client_section = config.get("client", {})
|
|
129
|
+
if isinstance(client_section, dict):
|
|
130
|
+
ssl_section = client_section.get("ssl", {})
|
|
131
|
+
if isinstance(ssl_section, dict) and any(
|
|
132
|
+
ssl_section.get(k) for k in ("cert", "cert_path", "key", "key_path")
|
|
133
|
+
):
|
|
134
|
+
return _ssl_paths_from_section(ssl_section)
|
|
135
|
+
server = config.get("server", {})
|
|
136
|
+
if not isinstance(server, dict):
|
|
137
|
+
return {}
|
|
138
|
+
return _ssl_paths_from_section(server.get("ssl", {}))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def adapter_settings_from_server_config(config: Mapping[str, Any]) -> dict[str, Any]:
|
|
142
|
+
"""Build adapter-style settings from a code-analysis server config dict."""
|
|
143
|
+
settings = dict(_network_from_server_config(config))
|
|
144
|
+
proto = str(settings.get("protocol", "")).lower()
|
|
145
|
+
if proto == "mtls":
|
|
146
|
+
settings["ssl"] = _client_mtls_paths_from_server_config(config)
|
|
147
|
+
elif proto == "https":
|
|
148
|
+
ssl_paths = _client_mtls_paths_from_server_config(config)
|
|
149
|
+
if ssl_paths.get("cert") and ssl_paths.get("key"):
|
|
150
|
+
settings["ssl"] = ssl_paths
|
|
151
|
+
return settings
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def adapter_settings_to_jsonrpc_kwargs(
|
|
155
|
+
settings: Mapping[str, Any],
|
|
156
|
+
*,
|
|
157
|
+
timeout: float | None = 60.0,
|
|
158
|
+
check_hostname: bool = False,
|
|
159
|
+
token_header: str | None = None,
|
|
160
|
+
token: str | None = None,
|
|
161
|
+
) -> dict[str, Any]:
|
|
162
|
+
"""Turn adapter-style settings into keyword args for JsonRpcClient."""
|
|
163
|
+
ssl_section = settings.get("ssl")
|
|
164
|
+
cert = key = ca = None
|
|
165
|
+
if isinstance(ssl_section, dict):
|
|
166
|
+
cert = ssl_section.get("cert") or ssl_section.get("cert_path")
|
|
167
|
+
key = ssl_section.get("key") or ssl_section.get("key_path")
|
|
168
|
+
ca = ssl_section.get("ca") or ssl_section.get("ca_path")
|
|
169
|
+
cert = _expand_path(cert) if cert else None
|
|
170
|
+
key = _expand_path(key) if key else None
|
|
171
|
+
ca = _expand_path(ca) if ca else None
|
|
172
|
+
|
|
173
|
+
th = token_header if token_header is not None else settings.get("token_header")
|
|
174
|
+
tok = token if token is not None else settings.get("token")
|
|
175
|
+
|
|
176
|
+
return {
|
|
177
|
+
"protocol": str(settings.get("protocol", "http")),
|
|
178
|
+
"host": str(settings.get("host", "127.0.0.1")),
|
|
179
|
+
"port": int(settings.get("port", 8080)),
|
|
180
|
+
"token_header": str(th) if th else None,
|
|
181
|
+
"token": str(tok) if tok else None,
|
|
182
|
+
"cert": cert,
|
|
183
|
+
"key": key,
|
|
184
|
+
"ca": ca,
|
|
185
|
+
"check_hostname": check_hostname,
|
|
186
|
+
"timeout": timeout,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def load_server_config(path: str | Path) -> dict[str, Any]:
|
|
191
|
+
"""Load server config.json into a dict."""
|
|
192
|
+
p = Path(path).expanduser()
|
|
193
|
+
with p.open("r", encoding="utf-8") as f:
|
|
194
|
+
data: Any = json.load(f)
|
|
195
|
+
if not isinstance(data, dict):
|
|
196
|
+
raise ValueError("Server config must be a JSON object")
|
|
197
|
+
# py3.10: return plain dict for mutability
|
|
198
|
+
return dict(data)
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client-side errors: parameter-validation failures and queued-job runtime errors.
|
|
3
|
+
|
|
4
|
+
No dependency on the code_analysis server package.
|
|
5
|
+
|
|
6
|
+
Author: Vasiliy Zdanovskiy
|
|
7
|
+
email: vasilyvz@gmail.com
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from typing import Any, Dict, Optional
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class ClientValidationError(ValueError):
|
|
16
|
+
"""Parameters do not match the command JSON schema, or the server refused a command.
|
|
17
|
+
|
|
18
|
+
``code`` is ``VALIDATION_ERROR`` for a client-side parameter failure; when the server
|
|
19
|
+
answered with a refusal, ``code`` is the exact server error code (for example
|
|
20
|
+
``SESSION_OPEN_IN_EDITOR``) and ``details`` is the server's error details object.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
code: str | int = "VALIDATION_ERROR"
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message: str,
|
|
28
|
+
*,
|
|
29
|
+
field: Optional[str] = None,
|
|
30
|
+
details: Optional[Dict[str, Any]] = None,
|
|
31
|
+
code: str | int | None = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
"""Initialize with optional field, details payload and the server error code."""
|
|
34
|
+
super().__init__(message)
|
|
35
|
+
self.field = field
|
|
36
|
+
self.details = details or {}
|
|
37
|
+
if code:
|
|
38
|
+
self.code = code
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class QueueJobError(RuntimeError):
|
|
42
|
+
"""Base for queued-job runtime errors (job failure, timeout, command failure).
|
|
43
|
+
|
|
44
|
+
Deliberately does **not** inherit :class:`ClientValidationError`. That class
|
|
45
|
+
represents bad input caught before a call is even sent, and an
|
|
46
|
+
``except ClientValidationError:`` handler written for that purpose must not
|
|
47
|
+
silently swallow a runtime failure that happened server-side after a valid
|
|
48
|
+
call was queued and polled. Catch ``QueueJobError`` (or its subclasses)
|
|
49
|
+
separately from parameter-validation errors.
|
|
50
|
+
"""
|
|
51
|
+
|
|
52
|
+
def __init__(
|
|
53
|
+
self,
|
|
54
|
+
message: str,
|
|
55
|
+
*,
|
|
56
|
+
job_id: Optional[str] = None,
|
|
57
|
+
details: Optional[Dict[str, Any]] = None,
|
|
58
|
+
) -> None:
|
|
59
|
+
"""Initialize with message, the job id involved, and a details payload."""
|
|
60
|
+
super().__init__(message)
|
|
61
|
+
self.job_id = job_id
|
|
62
|
+
self.details = details or {}
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class JobFailedError(QueueJobError):
|
|
66
|
+
"""A queued job was stopped or cancelled, or reported a queue-level error status.
|
|
67
|
+
|
|
68
|
+
A queued job that ended ``failed`` with the command's own error raises
|
|
69
|
+
:class:`CommandFailedError` instead (owner decision 2026-11-07, bug e58c5982).
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
job_id: Optional[str],
|
|
75
|
+
error: Any = None,
|
|
76
|
+
*,
|
|
77
|
+
status: Optional[str] = None,
|
|
78
|
+
) -> None:
|
|
79
|
+
"""Store job id, verbatim error payload, and terminal status."""
|
|
80
|
+
message = f"Job {job_id!r} failed (status={status!r}): {error!r}"
|
|
81
|
+
super().__init__(
|
|
82
|
+
message,
|
|
83
|
+
job_id=job_id,
|
|
84
|
+
details={"job_id": job_id, "status": status, "error": error},
|
|
85
|
+
)
|
|
86
|
+
self.error = error
|
|
87
|
+
self.status = status
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class JobTimeoutError(QueueJobError):
|
|
91
|
+
"""The caller wait exceeded ``timeout``; the call detached and the job keeps running server-side."""
|
|
92
|
+
|
|
93
|
+
def __init__(self, job_id: Optional[str], timeout: Optional[float]) -> None:
|
|
94
|
+
"""Store job id and the timeout that was exceeded."""
|
|
95
|
+
message = (
|
|
96
|
+
f"Timed out after {timeout}s waiting for job {job_id!r}; "
|
|
97
|
+
"the job keeps running server-side - wait for it again with "
|
|
98
|
+
"QueuedJob.wait or retry with a longer timeout."
|
|
99
|
+
)
|
|
100
|
+
super().__init__(
|
|
101
|
+
message,
|
|
102
|
+
job_id=job_id,
|
|
103
|
+
details={"job_id": job_id, "timeout": timeout},
|
|
104
|
+
)
|
|
105
|
+
self.timeout = timeout
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class CommandFailedError(QueueJobError):
|
|
109
|
+
"""A command ran but its inner result is an error envelope (``success`` false).
|
|
110
|
+
|
|
111
|
+
Raised for immediate and queued commands alike; a failing inner result is never
|
|
112
|
+
returned as a plain dict.
|
|
113
|
+
"""
|
|
114
|
+
|
|
115
|
+
def __init__(
|
|
116
|
+
self,
|
|
117
|
+
command: Optional[str],
|
|
118
|
+
job_id: Optional[str],
|
|
119
|
+
error: Any,
|
|
120
|
+
*,
|
|
121
|
+
result: Optional[Dict[str, Any]] = None,
|
|
122
|
+
) -> None:
|
|
123
|
+
"""Store command name, job id, the verbatim inner error object and the inner envelope."""
|
|
124
|
+
message = f"Command {command!r} failed (job_id={job_id!r}): {error!r}"
|
|
125
|
+
super().__init__(
|
|
126
|
+
message,
|
|
127
|
+
job_id=job_id,
|
|
128
|
+
details={"command": command, "job_id": job_id, "error": error},
|
|
129
|
+
)
|
|
130
|
+
self.command = command
|
|
131
|
+
self.error = error
|
|
132
|
+
self.result = result
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class VectorizerUnavailableError(RuntimeError):
|
|
136
|
+
"""The vectorizer (embedding or chunker service) is not available.
|
|
137
|
+
|
|
138
|
+
Raised by vectorizer-dependent commands (semantic_search, revectorize)
|
|
139
|
+
when the embedding or chunker service is known unavailable.
|
|
140
|
+
The pre-V1 contract EMBEDDING_SERVICE_ERROR is reserved for a failure of
|
|
141
|
+
the call itself, not for unavailability of a known-down service.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
code = "VECTORIZER_UNAVAILABLE"
|
|
145
|
+
|
|
146
|
+
def __init__(
|
|
147
|
+
self,
|
|
148
|
+
message: str,
|
|
149
|
+
*,
|
|
150
|
+
service: Optional[str] = None,
|
|
151
|
+
services: Optional[list[str]] = None,
|
|
152
|
+
reason: Optional[str] = None,
|
|
153
|
+
since: Optional[str] = None,
|
|
154
|
+
last_probe_at: Optional[str] = None,
|
|
155
|
+
retry_after_seconds: Optional[int] = None,
|
|
156
|
+
details: Optional[Dict[str, Any]] = None,
|
|
157
|
+
) -> None:
|
|
158
|
+
"""Initialize with message and vectorizer availability details.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
message: Human-readable error message
|
|
162
|
+
service: The first unavailable required service (embedding or chunker)
|
|
163
|
+
services: List of all unavailable required services
|
|
164
|
+
reason: Reason for unavailability (e.g., timeout, connection_refused)
|
|
165
|
+
since: ISO-8601 UTC timestamp of when the service became unavailable
|
|
166
|
+
last_probe_at: ISO-8601 UTC timestamp of the last probe attempt
|
|
167
|
+
retry_after_seconds: Suggested wait time before retrying
|
|
168
|
+
details: Additional details dict (overrides individual fields if provided)
|
|
169
|
+
"""
|
|
170
|
+
super().__init__(message)
|
|
171
|
+
if details is None:
|
|
172
|
+
details = {}
|
|
173
|
+
self.service = service or details.get("service")
|
|
174
|
+
self.services = services or details.get("services", [])
|
|
175
|
+
self.reason = reason or details.get("reason")
|
|
176
|
+
self.since = since or details.get("since")
|
|
177
|
+
self.last_probe_at = last_probe_at or details.get("last_probe_at")
|
|
178
|
+
self.retry_after_seconds = retry_after_seconds or details.get(
|
|
179
|
+
"retry_after_seconds"
|
|
180
|
+
)
|
|
181
|
+
self.details = {
|
|
182
|
+
"service": self.service,
|
|
183
|
+
"services": self.services,
|
|
184
|
+
"reason": self.reason,
|
|
185
|
+
"since": self.since,
|
|
186
|
+
"last_probe_at": self.last_probe_at,
|
|
187
|
+
"retry_after_seconds": self.retry_after_seconds,
|
|
188
|
+
}
|