ailogger-client-dev 0.1.51__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.
- ailogger_client/__init__.py +96 -0
- ailogger_client/client.py +220 -0
- ailogger_client/config.py +386 -0
- ailogger_client/errors.py +610 -0
- ailogger_client/file_transfer.py +110 -0
- ailogger_client/invocation.py +550 -0
- ailogger_client/message_methods.py +332 -0
- ailogger_client/observation_methods.py +195 -0
- ailogger_client/py.typed +0 -0
- ailogger_client/queued_methods.py +247 -0
- ailogger_client/read_methods.py +585 -0
- ailogger_client/retrieval_methods.py +314 -0
- ailogger_client/service_methods.py +148 -0
- ailogger_client/session_methods.py +236 -0
- ailogger_client_dev-0.1.51.dist-info/METADATA +108 -0
- ailogger_client_dev-0.1.51.dist-info/RECORD +18 -0
- ailogger_client_dev-0.1.51.dist-info/WHEEL +5 -0
- ailogger_client_dev-0.1.51.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ailogger service client.
|
|
3
|
+
|
|
4
|
+
This package provides the client for the ai-logger service, distributed and
|
|
5
|
+
installed independently of the service itself. It is a wrapper around the
|
|
6
|
+
adapter framework's ``JsonRpcClient`` (from ``mcp_proxy_adapter.client.jsonrpc_client``)
|
|
7
|
+
and adds only the dialogue vocabulary of the ai-logger service: the command
|
|
8
|
+
methods, the generic call surface and the typed error vocabulary.
|
|
9
|
+
|
|
10
|
+
The wrapper is constructed from a named section of the consuming process's
|
|
11
|
+
configuration document - the same sectioned shape every fleet client in the
|
|
12
|
+
ai-logger fleet uses: a top-level ``protocol``, a ``server`` sub-section
|
|
13
|
+
carrying host and port, a ``client`` sub-section that may carry ssl material
|
|
14
|
+
overriding the server's own, a top-level ``ssl`` sub-section used as a
|
|
15
|
+
fallback, and an ``auth`` sub-section carrying a token and the header it
|
|
16
|
+
travels in.
|
|
17
|
+
|
|
18
|
+
Transport, request serialization, response parsing, retries, job tracking,
|
|
19
|
+
queue operations, file transfer with integrity checking and connection
|
|
20
|
+
management all belong to the adapter framework's client: this package wraps
|
|
21
|
+
that client and adds only the dialogue vocabulary on top.
|
|
22
|
+
|
|
23
|
+
A-009 SECTION: Published Surface
|
|
24
|
+
|
|
25
|
+
The package's public surface is declared explicitly by ``__all__`` and holds
|
|
26
|
+
the client class, the construction entry points, and the complete typed error
|
|
27
|
+
vocabulary. A caller that wants to catch everything this client can raise
|
|
28
|
+
catches the single base kind (``AiLoggerClientError``), and every specific kind
|
|
29
|
+
is reachable from the package root. This closes the vocabulary: a consumer
|
|
30
|
+
never reaches into a private module path to catch a typed error of this client.
|
|
31
|
+
|
|
32
|
+
Deliberately unpublished from the package root (but still importable via
|
|
33
|
+
submodule paths like ``ailogger_client.errors`` or ``ailogger_client.client``):
|
|
34
|
+
the adapter framework's client class and its entire transport and exception
|
|
35
|
+
hierarchy; the nine mixin classes (InvocationMixin, QueuedMixin,
|
|
36
|
+
ObservationMixin, SessionMixin, MessageMixin, ReadMixin, RetrievalMixin,
|
|
37
|
+
ServiceMixin, FileTransferMixin)
|
|
38
|
+
that compose the client; the translation table and its supporting machinery
|
|
39
|
+
(``translate_framework_failure``, ``translated_failures``, ``refusal_from_answer``);
|
|
40
|
+
and the private attribute holding the framework client. This design keeps
|
|
41
|
+
this package a wrapper over the framework rather than a second way of
|
|
42
|
+
importing it, and ensures that transport details remain behind the client's
|
|
43
|
+
boundary.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
from __future__ import annotations
|
|
47
|
+
|
|
48
|
+
from .client import AiLoggerClient
|
|
49
|
+
from .config import (
|
|
50
|
+
ConfigError,
|
|
51
|
+
load_client_section,
|
|
52
|
+
load_config_kwargs,
|
|
53
|
+
load_config_kwargs_from_document,
|
|
54
|
+
)
|
|
55
|
+
from .errors import (
|
|
56
|
+
AiLoggerClientError,
|
|
57
|
+
CompletionFailedError,
|
|
58
|
+
CompletionTimeoutError,
|
|
59
|
+
DialogueFileTransferError,
|
|
60
|
+
InadmissibleArgumentValueError,
|
|
61
|
+
MissingArgumentError,
|
|
62
|
+
QueuedCompletionError,
|
|
63
|
+
QueuedProtocolError,
|
|
64
|
+
RequestRejectedError,
|
|
65
|
+
ResultUnavailableError,
|
|
66
|
+
SchemaDisagreementError,
|
|
67
|
+
ServiceRefusalError,
|
|
68
|
+
ServiceUnreachableError,
|
|
69
|
+
SyncFallbackWaitExhaustedError,
|
|
70
|
+
UnknownCommandError,
|
|
71
|
+
WrongArgumentTypeError,
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"AiLoggerClient",
|
|
76
|
+
"AiLoggerClientError",
|
|
77
|
+
"CompletionFailedError",
|
|
78
|
+
"CompletionTimeoutError",
|
|
79
|
+
"ConfigError",
|
|
80
|
+
"DialogueFileTransferError",
|
|
81
|
+
"InadmissibleArgumentValueError",
|
|
82
|
+
"MissingArgumentError",
|
|
83
|
+
"QueuedCompletionError",
|
|
84
|
+
"QueuedProtocolError",
|
|
85
|
+
"RequestRejectedError",
|
|
86
|
+
"ResultUnavailableError",
|
|
87
|
+
"SchemaDisagreementError",
|
|
88
|
+
"ServiceRefusalError",
|
|
89
|
+
"ServiceUnreachableError",
|
|
90
|
+
"SyncFallbackWaitExhaustedError",
|
|
91
|
+
"UnknownCommandError",
|
|
92
|
+
"WrongArgumentTypeError",
|
|
93
|
+
"load_client_section",
|
|
94
|
+
"load_config_kwargs",
|
|
95
|
+
"load_config_kwargs_from_document",
|
|
96
|
+
]
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Client for the ai-logger service.
|
|
3
|
+
|
|
4
|
+
This module declares the client class of the ai-logger service: the construction
|
|
5
|
+
and encapsulation boundary before any command of the surface reaches it.
|
|
6
|
+
|
|
7
|
+
CONSTRUCTION SHAPE. The client is built from its own named section of the
|
|
8
|
+
configuration document of the process that uses it, in the sectioned fleet shape,
|
|
9
|
+
by handing that section to the configuration translation of ailogger_client.config
|
|
10
|
+
and passing the resulting keyword arguments straight to the framework client's
|
|
11
|
+
constructor. Two construction routes are offered: from a section already read as
|
|
12
|
+
a mapping, and from a configuration document identified by path together with the
|
|
13
|
+
name of the section to read. Both routes end in the same single place where the
|
|
14
|
+
framework client is built, so there is exactly one construction path in the class.
|
|
15
|
+
|
|
16
|
+
ENCAPSULATION. The client holds an instance of JsonRpcClient from
|
|
17
|
+
mcp_proxy_adapter.client.jsonrpc_client on a private attribute. The held instance
|
|
18
|
+
leaves the object by no route: no public attribute, no property, no accessor method,
|
|
19
|
+
no return value, no argument default, no representation string or attribute-forwarding
|
|
20
|
+
hook. A caller holding this client cannot reach a connection through it. Inside the
|
|
21
|
+
package a single private route to the held instance exists, so the typed methods
|
|
22
|
+
have somewhere to call through without the attribute becoming part of the caller-facing
|
|
23
|
+
surface.
|
|
24
|
+
|
|
25
|
+
LIFECYCLE. Whatever opening and closing the client needs is delegated to the held
|
|
26
|
+
instance. The client supports use as an asynchronous context manager, so entering
|
|
27
|
+
and leaving a client is entering and leaving what the framework client already
|
|
28
|
+
provides. Closing twice is harmless.
|
|
29
|
+
|
|
30
|
+
CALLER SURFACE. No host, no connection, no retry, no status code and no transport
|
|
31
|
+
envelope appears in any argument, in any return value or in any readable attribute
|
|
32
|
+
of this class. The caller works only in the terms the service itself publishes.
|
|
33
|
+
"""
|
|
34
|
+
|
|
35
|
+
from __future__ import annotations
|
|
36
|
+
|
|
37
|
+
import json
|
|
38
|
+
from collections.abc import Mapping
|
|
39
|
+
from pathlib import Path
|
|
40
|
+
from typing import Any
|
|
41
|
+
|
|
42
|
+
from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient
|
|
43
|
+
|
|
44
|
+
from .config import (
|
|
45
|
+
ConfigError,
|
|
46
|
+
load_client_section,
|
|
47
|
+
load_config_kwargs,
|
|
48
|
+
load_sync_fallback_wait,
|
|
49
|
+
)
|
|
50
|
+
from .file_transfer import FileTransferMixin
|
|
51
|
+
from .invocation import InvocationMixin
|
|
52
|
+
from .message_methods import MessageMixin
|
|
53
|
+
from .observation_methods import ObservationMixin
|
|
54
|
+
from .queued_methods import QueuedMixin
|
|
55
|
+
from .read_methods import ReadMixin
|
|
56
|
+
from .retrieval_methods import RetrievalMixin
|
|
57
|
+
from .service_methods import ServiceMixin
|
|
58
|
+
from .session_methods import SessionMixin
|
|
59
|
+
|
|
60
|
+
__all__ = ["AiLoggerClient"]
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
class AiLoggerClient(
|
|
64
|
+
InvocationMixin,
|
|
65
|
+
QueuedMixin,
|
|
66
|
+
ObservationMixin,
|
|
67
|
+
SessionMixin,
|
|
68
|
+
MessageMixin,
|
|
69
|
+
ReadMixin,
|
|
70
|
+
RetrievalMixin,
|
|
71
|
+
ServiceMixin,
|
|
72
|
+
FileTransferMixin,
|
|
73
|
+
):
|
|
74
|
+
"""
|
|
75
|
+
Wrapper over the installed adapter framework's JsonRpcClient, adding the
|
|
76
|
+
dialogue vocabulary of the ai-logger service.
|
|
77
|
+
|
|
78
|
+
The client is constructed from the consuming process's configuration document,
|
|
79
|
+
in the sectioned fleet shape. It holds an instance of JsonRpcClient privately,
|
|
80
|
+
delegating all transport, connection, retry and job tracking to the framework
|
|
81
|
+
client.
|
|
82
|
+
|
|
83
|
+
The client supports use as an asynchronous context manager. Lifecycle operations
|
|
84
|
+
(enter, exit, close) are delegated to the held JsonRpcClient instance.
|
|
85
|
+
|
|
86
|
+
COMPOSITION (G-012/T-003/A-008). The client is assembled from nine mixin
|
|
87
|
+
classes (QueuedMixin, which owns the machinery of always-queued commands,
|
|
88
|
+
and ObservationMixin, which carries the probe, help, health-route and
|
|
89
|
+
answer-mode methods, precede the mixins whose typed methods build on
|
|
90
|
+
it), each contributing a set of typed methods to the caller-facing
|
|
91
|
+
surface:
|
|
92
|
+
InvocationMixin provides the private invocation seam and the generic call, plus
|
|
93
|
+
job observation; SessionMixin provides session lifecycle commands; MessageMixin
|
|
94
|
+
provides message append; ReadMixin provides the five session reads and session
|
|
95
|
+
listing; RetrievalMixin provides working-set assembly and similarity search;
|
|
96
|
+
ServiceMixin provides info and health; FileTransferMixin declares dialogue
|
|
97
|
+
import and export (not yet designed). Every caller-facing method reaches the
|
|
98
|
+
held JsonRpcClient through the single private _invoke entry point that
|
|
99
|
+
InvocationMixin establishes, so there is one seam and one route. The composition
|
|
100
|
+
is order-independent for callers: all typed methods are reachable on an instance
|
|
101
|
+
regardless of the base order, because each mixin declares disjoint public names.
|
|
102
|
+
"""
|
|
103
|
+
|
|
104
|
+
def __init__(
|
|
105
|
+
self,
|
|
106
|
+
section: Mapping[str, Any],
|
|
107
|
+
base_dir: str | None = None,
|
|
108
|
+
) -> None:
|
|
109
|
+
"""
|
|
110
|
+
Construct the client from a configuration section.
|
|
111
|
+
|
|
112
|
+
Args:
|
|
113
|
+
section: The "ailogger_client" section from the consuming process's
|
|
114
|
+
configuration document, as a mapping. Must carry the sectioned
|
|
115
|
+
fleet shape: a top-level ``protocol``, a ``server`` sub-section
|
|
116
|
+
with ``host`` and ``port``, optional ``client`` and ``ssl``
|
|
117
|
+
sub-sections, and an optional ``auth`` sub-section.
|
|
118
|
+
|
|
119
|
+
base_dir: When provided, the directory against which relative certificate
|
|
120
|
+
paths in the section are resolved. When absent, paths are resolved
|
|
121
|
+
against the calling process's current working directory.
|
|
122
|
+
|
|
123
|
+
Raises:
|
|
124
|
+
ConfigError: If the section is missing required keys or if any key
|
|
125
|
+
has the wrong type, including ``client.sync_fallback_wait_seconds``
|
|
126
|
+
that is not a number greater than zero.
|
|
127
|
+
"""
|
|
128
|
+
kwargs = load_config_kwargs(section, base_dir)
|
|
129
|
+
self._sync_fallback_wait_seconds: float = load_sync_fallback_wait(section)
|
|
130
|
+
self._client: JsonRpcClient = JsonRpcClient(**kwargs)
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def from_document_path(
|
|
134
|
+
cls,
|
|
135
|
+
path: str | Path,
|
|
136
|
+
section_name: str = "ailogger_client",
|
|
137
|
+
) -> AiLoggerClient:
|
|
138
|
+
"""
|
|
139
|
+
Construct the client from a configuration document file.
|
|
140
|
+
|
|
141
|
+
Reads the specified JSON configuration file, extracts the named section,
|
|
142
|
+
resolves relative certificate paths against the document's directory,
|
|
143
|
+
and constructs the client. This is a convenience route that combines
|
|
144
|
+
file reading, section extraction, and client construction.
|
|
145
|
+
|
|
146
|
+
Args:
|
|
147
|
+
path: The filesystem path to a JSON configuration document.
|
|
148
|
+
|
|
149
|
+
section_name: The name of the section to read from the document.
|
|
150
|
+
Defaults to "ailogger_client". This parameter exists for
|
|
151
|
+
flexibility but should remain at the default for the ai-logger client.
|
|
152
|
+
|
|
153
|
+
Returns:
|
|
154
|
+
An AiLoggerClient instance.
|
|
155
|
+
|
|
156
|
+
Raises:
|
|
157
|
+
ConfigError: If the document carries no section with the given name
|
|
158
|
+
or if the section is malformed.
|
|
159
|
+
FileNotFoundError: If the file does not exist.
|
|
160
|
+
json.JSONDecodeError: If the file is not valid JSON.
|
|
161
|
+
"""
|
|
162
|
+
path_obj = Path(path).resolve()
|
|
163
|
+
with open(path_obj) as f:
|
|
164
|
+
document = json.load(f)
|
|
165
|
+
|
|
166
|
+
base_dir = str(path_obj.parent)
|
|
167
|
+
|
|
168
|
+
# Extract the section from the document
|
|
169
|
+
if section_name == "ailogger_client":
|
|
170
|
+
section = load_client_section(document)
|
|
171
|
+
else:
|
|
172
|
+
section = document.get(section_name)
|
|
173
|
+
if not isinstance(section, Mapping):
|
|
174
|
+
raise ConfigError(
|
|
175
|
+
f"configuration document carries no '{section_name}' "
|
|
176
|
+
"section for this client"
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
# Use the main constructor (single construction path)
|
|
180
|
+
return cls(section, base_dir=base_dir)
|
|
181
|
+
|
|
182
|
+
async def close(self) -> None:
|
|
183
|
+
"""
|
|
184
|
+
Close the underlying transport and release resources.
|
|
185
|
+
|
|
186
|
+
Delegates to the held JsonRpcClient's close method. Closing twice
|
|
187
|
+
is harmless; the underlying client handles idempotency.
|
|
188
|
+
"""
|
|
189
|
+
await self._client.close()
|
|
190
|
+
|
|
191
|
+
async def __aenter__(self) -> AiLoggerClient:
|
|
192
|
+
"""
|
|
193
|
+
Async context manager entry.
|
|
194
|
+
|
|
195
|
+
Returns self for use in `async with` statements. The held JsonRpcClient
|
|
196
|
+
does not require explicit initialization, so no setup is performed here.
|
|
197
|
+
"""
|
|
198
|
+
return self
|
|
199
|
+
|
|
200
|
+
async def __aexit__(
|
|
201
|
+
self,
|
|
202
|
+
exc_type: type[BaseException] | None,
|
|
203
|
+
exc_val: BaseException | None,
|
|
204
|
+
exc_tb: Any,
|
|
205
|
+
) -> None:
|
|
206
|
+
"""
|
|
207
|
+
Async context manager exit.
|
|
208
|
+
|
|
209
|
+
Closes the underlying transport and releases resources by delegating
|
|
210
|
+
to close().
|
|
211
|
+
"""
|
|
212
|
+
await self.close()
|
|
213
|
+
|
|
214
|
+
def __repr__(self) -> str:
|
|
215
|
+
"""
|
|
216
|
+
Return a string representation of the client.
|
|
217
|
+
|
|
218
|
+
Does not expose the held instance or any transport details.
|
|
219
|
+
"""
|
|
220
|
+
return f"{self.__class__.__name__}()"
|
|
@@ -0,0 +1,386 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Configuration loading for the ai-logger client.
|
|
3
|
+
|
|
4
|
+
This module reads the ai-logger client section from a consuming process's
|
|
5
|
+
configuration document and produces the keyword arguments the JsonRpcClient
|
|
6
|
+
constructor requires. It is the only place in this client package where a
|
|
7
|
+
configuration value is read.
|
|
8
|
+
|
|
9
|
+
CONSTRUCTION SHAPE. The client section is named "ailogger_client" and carries:
|
|
10
|
+
a top-level ``protocol`` (http, https, or mtls), a ``server`` sub-section with
|
|
11
|
+
``host`` and ``port``, a ``client`` sub-section that may carry ssl material
|
|
12
|
+
overriding the server's own, a top-level ``ssl`` sub-section used as a fallback,
|
|
13
|
+
and an ``auth`` sub-section carrying a ``token`` and the HTTP header name
|
|
14
|
+
``token_header`` the token travels in.
|
|
15
|
+
|
|
16
|
+
The section is read from the configuration document of the consuming process,
|
|
17
|
+
never from the service's own document: a consumer runs in another process and
|
|
18
|
+
often on another host, so reading the service's own configuration would
|
|
19
|
+
introduce a second notion of where the service lives.
|
|
20
|
+
|
|
21
|
+
WHAT THIS MODULE PRODUCES. Exactly the keyword arguments the JsonRpcClient
|
|
22
|
+
constructor takes: protocol, host, port, token_header, token, cert, key, ca,
|
|
23
|
+
check_hostname and timeout. Certificate material is passed as absolute filesystem
|
|
24
|
+
paths for the client to use, never loaded or parsed here. The set is derived
|
|
25
|
+
from the JsonRpcClient constructor signature, verified at module load time.
|
|
26
|
+
|
|
27
|
+
WHAT THIS MODULE MUST NOT DO. It never builds a URL, base address or endpoint
|
|
28
|
+
string. It never loads, parses or turns certificate material into a security
|
|
29
|
+
context. It never opens a connection, socket or session. It reads no environment
|
|
30
|
+
variable to locate the service and writes no service address into itself. It
|
|
31
|
+
imports nothing from the service's own package, and makes no network call.
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
from __future__ import annotations
|
|
35
|
+
|
|
36
|
+
import inspect
|
|
37
|
+
import math
|
|
38
|
+
import os
|
|
39
|
+
from collections.abc import Mapping
|
|
40
|
+
from pathlib import Path
|
|
41
|
+
from typing import Any, Final
|
|
42
|
+
|
|
43
|
+
from mcp_proxy_adapter.client.jsonrpc_client import JsonRpcClient
|
|
44
|
+
|
|
45
|
+
__all__ = [
|
|
46
|
+
"SYNC_FALLBACK_WAIT_SECONDS",
|
|
47
|
+
"ConfigError",
|
|
48
|
+
"load_client_section",
|
|
49
|
+
"load_config_kwargs",
|
|
50
|
+
"load_config_kwargs_from_document",
|
|
51
|
+
"load_sync_fallback_wait",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
# The client owns these existing constructor parameters. The adapter may add
|
|
56
|
+
# explicitly allowed optional keyword-only extensions without breaking import.
|
|
57
|
+
_REQUIRED_KWARGS = frozenset(
|
|
58
|
+
{
|
|
59
|
+
"protocol",
|
|
60
|
+
"host",
|
|
61
|
+
"port",
|
|
62
|
+
"token_header",
|
|
63
|
+
"token",
|
|
64
|
+
"cert",
|
|
65
|
+
"key",
|
|
66
|
+
"ca",
|
|
67
|
+
"check_hostname",
|
|
68
|
+
"timeout",
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
# http_socket_options: adapter 8.11.3 and later. ws_config: adapter 8.11.9 and
|
|
72
|
+
# later. The client passes neither; each is optional on both adapter lines.
|
|
73
|
+
_OPTIONAL_EXTENSION_KWARGS = frozenset({"http_socket_options", "ws_config"})
|
|
74
|
+
_LIVE_SIGNATURE = inspect.signature(JsonRpcClient.__init__)
|
|
75
|
+
_LIVE_KWARGS = frozenset(_LIVE_SIGNATURE.parameters) - {"self"}
|
|
76
|
+
_UNEXPECTED_KWARGS = _LIVE_KWARGS - _REQUIRED_KWARGS - _OPTIONAL_EXTENSION_KWARGS
|
|
77
|
+
_MISSING_KWARGS = _REQUIRED_KWARGS - _LIVE_KWARGS
|
|
78
|
+
_LIVE_OPTIONAL_EXTENSIONS = tuple(
|
|
79
|
+
_LIVE_SIGNATURE.parameters[name]
|
|
80
|
+
for name in sorted(_OPTIONAL_EXTENSION_KWARGS)
|
|
81
|
+
if name in _LIVE_SIGNATURE.parameters
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
if (
|
|
85
|
+
_MISSING_KWARGS
|
|
86
|
+
or _UNEXPECTED_KWARGS
|
|
87
|
+
or any(
|
|
88
|
+
extension.kind is not inspect.Parameter.KEYWORD_ONLY
|
|
89
|
+
or extension.default is inspect.Parameter.empty
|
|
90
|
+
for extension in _LIVE_OPTIONAL_EXTENSIONS
|
|
91
|
+
)
|
|
92
|
+
):
|
|
93
|
+
raise RuntimeError(
|
|
94
|
+
"JsonRpcClient constructor signature has diverged from supported kwargs. "
|
|
95
|
+
f"Required: {sorted(_REQUIRED_KWARGS)}, "
|
|
96
|
+
f"Allowed optional extensions: {sorted(_OPTIONAL_EXTENSION_KWARGS)}, "
|
|
97
|
+
f"Live: {sorted(_LIVE_KWARGS)}"
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
#: How long the client waits, in seconds, for the queued job of a command that
|
|
102
|
+
#: overran the service's sync cap, when ``client.sync_fallback_wait_seconds`` is
|
|
103
|
+
#: not configured.
|
|
104
|
+
SYNC_FALLBACK_WAIT_SECONDS: Final[float] = 600.0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
class ConfigError(Exception):
|
|
108
|
+
"""Raised when configuration is invalid or missing."""
|
|
109
|
+
|
|
110
|
+
pass
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
def _get_nested(section: Mapping[str, Any], *keys: str) -> Any:
|
|
114
|
+
"""
|
|
115
|
+
Read a nested value from ``section``, returning None if any level is
|
|
116
|
+
absent or non-mapping.
|
|
117
|
+
"""
|
|
118
|
+
node: Any = section
|
|
119
|
+
for key in keys:
|
|
120
|
+
if not isinstance(node, Mapping):
|
|
121
|
+
return None
|
|
122
|
+
node = node.get(key)
|
|
123
|
+
return node
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _to_absolute_path(path: str | None, base_dir: str | None = None) -> str | None:
|
|
127
|
+
"""
|
|
128
|
+
Convert a path to an absolute filesystem path. Returns None if path is None.
|
|
129
|
+
|
|
130
|
+
When ``base_dir`` is provided, paths are resolved relative to that directory,
|
|
131
|
+
making the result independent of the calling process's working directory.
|
|
132
|
+
|
|
133
|
+
When ``base_dir`` is None (bare mapping route with no document context),
|
|
134
|
+
paths are resolved relative to the current working directory. In this case,
|
|
135
|
+
the result depends on the caller's cwd at call time.
|
|
136
|
+
"""
|
|
137
|
+
if path is None:
|
|
138
|
+
return None
|
|
139
|
+
if base_dir is not None:
|
|
140
|
+
return str(Path(base_dir) / path).replace("//", "/")
|
|
141
|
+
return os.path.abspath(path)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
def _extract_server_config(
|
|
145
|
+
section: Mapping[str, Any],
|
|
146
|
+
) -> tuple[str, int, str]:
|
|
147
|
+
"""
|
|
148
|
+
Extract protocol, host, and port from section. Host and port are required.
|
|
149
|
+
Protocol defaults to 'http' if absent.
|
|
150
|
+
"""
|
|
151
|
+
protocol = section.get("protocol", "http")
|
|
152
|
+
if not isinstance(protocol, str):
|
|
153
|
+
raise ConfigError(f"protocol must be a string, got {type(protocol).__name__}")
|
|
154
|
+
|
|
155
|
+
server_section = section.get("server")
|
|
156
|
+
if not isinstance(server_section, Mapping):
|
|
157
|
+
raise ConfigError("server section must be a mapping")
|
|
158
|
+
|
|
159
|
+
host = server_section.get("host")
|
|
160
|
+
if not isinstance(host, str):
|
|
161
|
+
host_type = type(host).__name__ if host is not None else "None"
|
|
162
|
+
raise ConfigError(f"server.host must be a string, got {host_type}")
|
|
163
|
+
|
|
164
|
+
port = server_section.get("port")
|
|
165
|
+
if not isinstance(port, int):
|
|
166
|
+
port_type = type(port).__name__ if port is not None else "None"
|
|
167
|
+
raise ConfigError(f"server.port must be an integer, got {port_type}")
|
|
168
|
+
|
|
169
|
+
return protocol, port, host
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _extract_timeout(client_section: Mapping[str, Any] | None) -> float | None:
|
|
173
|
+
"""Extract timeout from client section, or None if absent."""
|
|
174
|
+
if not isinstance(client_section, Mapping):
|
|
175
|
+
return None
|
|
176
|
+
resolved_timeout = client_section.get("timeout")
|
|
177
|
+
if resolved_timeout is None:
|
|
178
|
+
return None
|
|
179
|
+
if isinstance(resolved_timeout, (int, float)):
|
|
180
|
+
return float(resolved_timeout)
|
|
181
|
+
raise ConfigError(
|
|
182
|
+
f"client.timeout must be a number, got {type(resolved_timeout).__name__}"
|
|
183
|
+
)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def load_sync_fallback_wait(section: Mapping[str, Any]) -> float:
|
|
187
|
+
"""
|
|
188
|
+
The wait, in seconds, for the queued job of a command that overran the
|
|
189
|
+
service's sync cap: ``client.sync_fallback_wait_seconds`` of the section, or
|
|
190
|
+
``SYNC_FALLBACK_WAIT_SECONDS`` when it is absent.
|
|
191
|
+
|
|
192
|
+
Raises:
|
|
193
|
+
ConfigError: If the configured value is not a number greater than zero
|
|
194
|
+
(a string, a boolean, zero, a negative number, null or a
|
|
195
|
+
non-finite number is refused).
|
|
196
|
+
"""
|
|
197
|
+
client_section = section.get("client")
|
|
198
|
+
if not isinstance(client_section, Mapping):
|
|
199
|
+
return SYNC_FALLBACK_WAIT_SECONDS
|
|
200
|
+
if "sync_fallback_wait_seconds" not in client_section:
|
|
201
|
+
return SYNC_FALLBACK_WAIT_SECONDS
|
|
202
|
+
wait = client_section["sync_fallback_wait_seconds"]
|
|
203
|
+
if (
|
|
204
|
+
isinstance(wait, bool)
|
|
205
|
+
or not isinstance(wait, (int, float))
|
|
206
|
+
or not math.isfinite(wait)
|
|
207
|
+
or wait <= 0
|
|
208
|
+
):
|
|
209
|
+
raise ConfigError(
|
|
210
|
+
"client.sync_fallback_wait_seconds must be a number greater than 0, "
|
|
211
|
+
f"got {wait!r}"
|
|
212
|
+
)
|
|
213
|
+
return float(wait)
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def _extract_ssl_config(
|
|
217
|
+
section: Mapping[str, Any],
|
|
218
|
+
client_section: Mapping[str, Any] | None,
|
|
219
|
+
base_dir: str | None = None,
|
|
220
|
+
) -> tuple[str | None, str | None, str | None]:
|
|
221
|
+
"""
|
|
222
|
+
Extract and validate cert, key, ca from client.ssl (taking precedence)
|
|
223
|
+
or ssl section (fallback). Resolves paths to absolute paths.
|
|
224
|
+
"""
|
|
225
|
+
cert: str | None = None
|
|
226
|
+
key: str | None = None
|
|
227
|
+
ca: str | None = None
|
|
228
|
+
|
|
229
|
+
if isinstance(client_section, Mapping):
|
|
230
|
+
client_ssl = client_section.get("ssl")
|
|
231
|
+
if isinstance(client_ssl, Mapping):
|
|
232
|
+
cert = client_ssl.get("cert")
|
|
233
|
+
key = client_ssl.get("key")
|
|
234
|
+
ca = client_ssl.get("ca")
|
|
235
|
+
|
|
236
|
+
ssl_section = section.get("ssl")
|
|
237
|
+
if isinstance(ssl_section, Mapping):
|
|
238
|
+
if cert is None:
|
|
239
|
+
cert = ssl_section.get("cert")
|
|
240
|
+
if key is None:
|
|
241
|
+
key = ssl_section.get("key")
|
|
242
|
+
if ca is None:
|
|
243
|
+
ca = ssl_section.get("ca")
|
|
244
|
+
|
|
245
|
+
# Validate types
|
|
246
|
+
if cert is not None and not isinstance(cert, str):
|
|
247
|
+
raise ConfigError(f"cert must be a string path, got {type(cert).__name__}")
|
|
248
|
+
if key is not None and not isinstance(key, str):
|
|
249
|
+
raise ConfigError(f"key must be a string path, got {type(key).__name__}")
|
|
250
|
+
if ca is not None and not isinstance(ca, str):
|
|
251
|
+
raise ConfigError(f"ca must be a string path, got {type(ca).__name__}")
|
|
252
|
+
|
|
253
|
+
cert_abs = _to_absolute_path(cert, base_dir)
|
|
254
|
+
key_abs = _to_absolute_path(key, base_dir)
|
|
255
|
+
ca_abs = _to_absolute_path(ca, base_dir)
|
|
256
|
+
|
|
257
|
+
return cert_abs, key_abs, ca_abs
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _extract_check_hostname(ssl_section: Mapping[str, Any] | None) -> bool:
|
|
261
|
+
"""Extract check_hostname from ssl section, defaulting to False."""
|
|
262
|
+
if not isinstance(ssl_section, Mapping):
|
|
263
|
+
return False
|
|
264
|
+
resolved_check_hostname = ssl_section.get("check_hostname")
|
|
265
|
+
if resolved_check_hostname is None:
|
|
266
|
+
return False
|
|
267
|
+
if isinstance(resolved_check_hostname, bool):
|
|
268
|
+
return resolved_check_hostname
|
|
269
|
+
raise ConfigError(
|
|
270
|
+
f"ssl.check_hostname must be a boolean, "
|
|
271
|
+
f"got {type(resolved_check_hostname).__name__}"
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
|
|
275
|
+
def _extract_auth_config(
|
|
276
|
+
auth_section: Mapping[str, Any] | None,
|
|
277
|
+
) -> tuple[str | None, str | None]:
|
|
278
|
+
"""Extract token and token_header from auth section."""
|
|
279
|
+
if not isinstance(auth_section, Mapping):
|
|
280
|
+
return None, None
|
|
281
|
+
|
|
282
|
+
token = auth_section.get("token")
|
|
283
|
+
token_header = auth_section.get("token_header")
|
|
284
|
+
|
|
285
|
+
if token is not None and not isinstance(token, str):
|
|
286
|
+
raise ConfigError(f"auth.token must be a string, got {type(token).__name__}")
|
|
287
|
+
if token_header is not None and not isinstance(token_header, str):
|
|
288
|
+
token_header_type = type(token_header).__name__
|
|
289
|
+
raise ConfigError(
|
|
290
|
+
f"auth.token_header must be a string, got {token_header_type}"
|
|
291
|
+
)
|
|
292
|
+
|
|
293
|
+
return token, token_header
|
|
294
|
+
|
|
295
|
+
|
|
296
|
+
def load_client_section(document: Mapping[str, Any]) -> Mapping[str, Any]:
|
|
297
|
+
"""
|
|
298
|
+
Extract the "ailogger_client" configuration section from a consuming
|
|
299
|
+
process's configuration document.
|
|
300
|
+
|
|
301
|
+
Raises ConfigError if the document carries no "ailogger_client" section.
|
|
302
|
+
|
|
303
|
+
Returns the section mapping for further processing.
|
|
304
|
+
"""
|
|
305
|
+
section = document.get("ailogger_client")
|
|
306
|
+
if not isinstance(section, Mapping):
|
|
307
|
+
raise ConfigError(
|
|
308
|
+
"configuration document carries no 'ailogger_client' "
|
|
309
|
+
"section for this client"
|
|
310
|
+
)
|
|
311
|
+
return section
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
def load_config_kwargs(
|
|
315
|
+
section: Mapping[str, Any], base_dir: str | None = None
|
|
316
|
+
) -> dict[str, Any]:
|
|
317
|
+
"""
|
|
318
|
+
Read the ai-logger client configuration section and produce the keyword
|
|
319
|
+
arguments the JsonRpcClient constructor requires.
|
|
320
|
+
|
|
321
|
+
``section`` must be the mapping from a consuming process's configuration
|
|
322
|
+
document; this function does not read a configuration file, resolve a path,
|
|
323
|
+
or read an environment variable itself.
|
|
324
|
+
|
|
325
|
+
``base_dir``, when provided, is the directory against which relative
|
|
326
|
+
certificate paths in the section are resolved. When absent, paths are
|
|
327
|
+
resolved against the calling process's current working directory.
|
|
328
|
+
|
|
329
|
+
Raises ConfigError if ``section`` is missing or if required keys are absent
|
|
330
|
+
or have the wrong type.
|
|
331
|
+
|
|
332
|
+
Returns a dict with keys: protocol, host, port, token_header, token, cert,
|
|
333
|
+
key, ca, check_hostname, timeout. Each key's value is the type JsonRpcClient
|
|
334
|
+
expects. Certificate paths, if present, are absolute paths.
|
|
335
|
+
"""
|
|
336
|
+
protocol, port, host = _extract_server_config(section)
|
|
337
|
+
|
|
338
|
+
client_section = section.get("client")
|
|
339
|
+
timeout = _extract_timeout(client_section)
|
|
340
|
+
|
|
341
|
+
cert, key, ca = _extract_ssl_config(section, client_section, base_dir)
|
|
342
|
+
|
|
343
|
+
ssl_section = section.get("ssl")
|
|
344
|
+
check_hostname = _extract_check_hostname(ssl_section)
|
|
345
|
+
|
|
346
|
+
auth_section = section.get("auth")
|
|
347
|
+
token, token_header = _extract_auth_config(auth_section)
|
|
348
|
+
|
|
349
|
+
# Build and return the kwargs dict
|
|
350
|
+
kwargs: dict[str, Any] = {
|
|
351
|
+
"protocol": protocol,
|
|
352
|
+
"host": host,
|
|
353
|
+
"port": port,
|
|
354
|
+
"token_header": token_header,
|
|
355
|
+
"token": token,
|
|
356
|
+
"cert": cert,
|
|
357
|
+
"key": key,
|
|
358
|
+
"ca": ca,
|
|
359
|
+
"check_hostname": check_hostname,
|
|
360
|
+
"timeout": timeout,
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return kwargs
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
def load_config_kwargs_from_document(
|
|
367
|
+
document: Mapping[str, Any], base_dir: str | None = None
|
|
368
|
+
) -> dict[str, Any]:
|
|
369
|
+
"""
|
|
370
|
+
Extract the "ailogger_client" section from a consuming process's
|
|
371
|
+
configuration document and produce the keyword arguments the JsonRpcClient
|
|
372
|
+
constructor requires. Combines load_client_section and load_config_kwargs
|
|
373
|
+
in a single call.
|
|
374
|
+
|
|
375
|
+
``base_dir``, when provided, is the directory against which relative
|
|
376
|
+
certificate paths are resolved. When absent, paths are resolved against
|
|
377
|
+
the calling process's current working directory.
|
|
378
|
+
|
|
379
|
+
Raises ConfigError if the document carries no "ailogger_client" section
|
|
380
|
+
or if the section is malformed.
|
|
381
|
+
|
|
382
|
+
Returns a dict with keys: protocol, host, port, token_header, token, cert,
|
|
383
|
+
key, ca, check_hostname, timeout.
|
|
384
|
+
"""
|
|
385
|
+
section = load_client_section(document)
|
|
386
|
+
return load_config_kwargs(section, base_dir)
|