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,110 @@
|
|
|
1
|
+
"""
|
|
2
|
+
File transfer methods for dialogue import and export.
|
|
3
|
+
|
|
4
|
+
This module carries the single mixin class that names the two methods a caller
|
|
5
|
+
would use to import a dialogue from a file and to export a dialogue to a file.
|
|
6
|
+
Import and export are not designed yet: neither method does transfer work when
|
|
7
|
+
called. Each is fully declared and fully annotated, and each raises
|
|
8
|
+
NotImplementedError with a message that states plainly that import and export
|
|
9
|
+
are not designed yet and that transfer will be built on the framework client's
|
|
10
|
+
own transfer surface (upload_file, resume_upload, download_file,
|
|
11
|
+
resume_download, execute_command_with_file) when they are.
|
|
12
|
+
|
|
13
|
+
The framework client held privately by the client object carries the complete
|
|
14
|
+
chunked transfer surface. This client wraps that framework client and exposes
|
|
15
|
+
only the dialogue vocabulary of the ai-logger service. But the dialogue surface
|
|
16
|
+
has no command that carries a file - no message carries one and no read returns
|
|
17
|
+
one. Transfer between this client and this service is wanted for exactly one
|
|
18
|
+
purpose: importing a dialogue from a file and exporting a dialogue to a file.
|
|
19
|
+
That purpose is not designed yet, and no step of the plan owns its design.
|
|
20
|
+
|
|
21
|
+
A consumer that reaches for dialogue import or export should meet a definite
|
|
22
|
+
answer rather than a missing attribute error, and the eventual work should have
|
|
23
|
+
a named place already established for it.
|
|
24
|
+
|
|
25
|
+
NETWORK LAW. This module imports no aiohttp, httpx, requests, fastapi, uvicorn
|
|
26
|
+
or standalone websocket client; opens no socket and no file; assemble no
|
|
27
|
+
address, connection, session or security context by hand; and builds no request
|
|
28
|
+
envelope. This module adds no transport of its own: the privately held framework
|
|
29
|
+
client carries all transfer, and nothing here touches it. Every declaration in
|
|
30
|
+
this file is fully annotated for the pipeline's named strict type check, and the
|
|
31
|
+
module satisfies its named lint check.
|
|
32
|
+
|
|
33
|
+
BOUNDARIES. This module declares no typed method for any command of the service
|
|
34
|
+
surface. It does not add a generic call, a completion default, a non-blocking
|
|
35
|
+
path or any job-observing call. It defines no error vocabulary and does not
|
|
36
|
+
fold this refusal into any error type: NotImplementedError is the language's
|
|
37
|
+
own. Import and export are not designed here and remain unimplemented: they
|
|
38
|
+
will be built on the framework client's transfer surface (upload_file,
|
|
39
|
+
download_file, execute_command_with_file, etc.) when design work completes.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
from __future__ import annotations
|
|
43
|
+
|
|
44
|
+
__all__ = ["FileTransferMixin"]
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class FileTransferMixin:
|
|
48
|
+
"""Mixin providing methods for dialogue file transfer.
|
|
49
|
+
|
|
50
|
+
Declares the two methods by which a caller would import a dialogue from a
|
|
51
|
+
file and export a dialogue to a file. Import and export are not designed
|
|
52
|
+
yet: each method raises NotImplementedError when called, with a message
|
|
53
|
+
stating plainly that import and export are not designed yet and that
|
|
54
|
+
transfer will be built on the framework client's own transfer surface when
|
|
55
|
+
they are.
|
|
56
|
+
|
|
57
|
+
This mixin does not compose with InvocationMixin and does not touch the
|
|
58
|
+
framework client's transfer surface. When import and export are eventually
|
|
59
|
+
designed, they will be built on that surface rather than on anything in
|
|
60
|
+
this mixin.
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
async def import_dialogue(self, source_path: str) -> None:
|
|
64
|
+
"""Import a dialogue from a file.
|
|
65
|
+
|
|
66
|
+
Dialogue import is not designed yet. When called, this method raises
|
|
67
|
+
NotImplementedError. Transfer will be built on the framework client's
|
|
68
|
+
own transfer surface (upload_file, resume_upload,
|
|
69
|
+
execute_command_with_file) when import design completes.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
source_path: Path to the file containing the dialogue to import.
|
|
73
|
+
|
|
74
|
+
Raises:
|
|
75
|
+
NotImplementedError: Dialogue import is not designed yet. Transfer
|
|
76
|
+
will be built on the framework client's transfer surface when
|
|
77
|
+
designed.
|
|
78
|
+
"""
|
|
79
|
+
raise NotImplementedError(
|
|
80
|
+
"Dialogue import is not designed yet. Transfer will be built on "
|
|
81
|
+
"the framework client's transfer surface when designed."
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
async def export_dialogue(self, destination_path: str) -> None:
|
|
85
|
+
"""Export a dialogue to a file.
|
|
86
|
+
|
|
87
|
+
Dialogue export is not designed yet. When called, this method raises
|
|
88
|
+
NotImplementedError. Transfer will be built on the framework client's
|
|
89
|
+
own transfer surface (download_file, resume_download,
|
|
90
|
+
execute_command_with_file) when export design completes.
|
|
91
|
+
|
|
92
|
+
Args:
|
|
93
|
+
destination_path: Path where the exported dialogue file will be
|
|
94
|
+
written.
|
|
95
|
+
|
|
96
|
+
Raises:
|
|
97
|
+
NotImplementedError: Dialogue export is not designed yet. Transfer
|
|
98
|
+
will be built on the framework client's transfer surface when
|
|
99
|
+
designed.
|
|
100
|
+
"""
|
|
101
|
+
raise NotImplementedError(
|
|
102
|
+
"Dialogue export is not designed yet. Transfer will be built on "
|
|
103
|
+
"the framework client's transfer surface when designed."
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# TODO(G-012/T-002/A-010): dialogue import/export deliberately deferred -- no
|
|
108
|
+
# step of the plan owns their design yet; build on the framework client's
|
|
109
|
+
# transfer surface (upload_file/download_file/execute_command_with_file) when
|
|
110
|
+
# designed.
|
|
@@ -0,0 +1,550 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Invocation entry point for the ai-logger client.
|
|
3
|
+
|
|
4
|
+
This module declares the single seam through which every caller-facing method of
|
|
5
|
+
the client reaches the service. It carries no typed method of any command, only:
|
|
6
|
+
|
|
7
|
+
- A private invocation entry point that takes a command name and a mapping of
|
|
8
|
+
parameters, carries them unchanged to the framework client's own invocation
|
|
9
|
+
surface, and returns what that surface returns.
|
|
10
|
+
|
|
11
|
+
- A public generic call that takes a command name and a mapping of parameters
|
|
12
|
+
and reaches any command of the service, including a command the service
|
|
13
|
+
publishes after this client was released, so a growing surface stays usable
|
|
14
|
+
from an already installed client.
|
|
15
|
+
|
|
16
|
+
The private entry point is a naming, not machinery: request and answer
|
|
17
|
+
serialisation, connection handling, retries, timeouts, OpenAPI schema fetching,
|
|
18
|
+
and schema validation are the framework client's own work and are reached
|
|
19
|
+
through the held instance. No request envelope, no JSON-RPC payload, no address,
|
|
20
|
+
no header set, and no transport concept in any signature, argument or return
|
|
21
|
+
type appears here.
|
|
22
|
+
|
|
23
|
+
NETWORK LAW. This module imports nothing from the adapter framework's
|
|
24
|
+
implementation; it does not reach the framework client except through the single
|
|
25
|
+
private route (_client attribute) and calls only the methods it publishes.
|
|
26
|
+
Connection handling, retries, timeouts, and all other transport concerns remain
|
|
27
|
+
the framework's responsibility.
|
|
28
|
+
|
|
29
|
+
COMPOSITION. This module defines a mixin class, intended to be composed into
|
|
30
|
+
the AiLoggerClient so the typed methods added by the plan's later steps reach
|
|
31
|
+
the service through the single invocation entry point here.
|
|
32
|
+
|
|
33
|
+
JOB OBSERVATION. When a command is executed with wait_for_result=False and the
|
|
34
|
+
service queues the command, the client returns a job identity envelope. The
|
|
35
|
+
caller may then use the job observation methods (get_job_status, get_job_messages,
|
|
36
|
+
cancel_command) to track the job's progress. These methods are not typed commands
|
|
37
|
+
of the service; they are observation surfaces over the framework client's own
|
|
38
|
+
job-tracking API and are not counted in the client-surface-agreement comparison.
|
|
39
|
+
|
|
40
|
+
A-007 SECTION: Guarded Seam and Refusal Recognition
|
|
41
|
+
|
|
42
|
+
Every route through which this module reaches the privately held framework
|
|
43
|
+
client is wrapped in the reusable translation guard (translated_failures) from
|
|
44
|
+
G-012/T-003/A-002. The guard catches framework ClientError, translates it to
|
|
45
|
+
this client's vocabulary via translate_framework_failure, and raises it with the
|
|
46
|
+
original as its cause. Plain programming errors (ValueError, RuntimeError) and
|
|
47
|
+
the NotImplementedError from undesigned file-transfer methods propagate
|
|
48
|
+
unchanged.
|
|
49
|
+
|
|
50
|
+
The answer-borne refusal recogniser (refusal_from_answer) is applied exactly
|
|
51
|
+
once: in _invoke, on the completed answer (the default wait_for_result=True
|
|
52
|
+
path), immediately before the return, so a refusal carried intact over a working
|
|
53
|
+
transport becomes the same service-refusal kind as one that arrives as a raised
|
|
54
|
+
failure. The non-blocking queued envelope and the job-observing calls' answers
|
|
55
|
+
are not passed through the recogniser. Both routes (raised and answer-borne)
|
|
56
|
+
end in the same ServiceRefusalError kind.
|
|
57
|
+
"""
|
|
58
|
+
|
|
59
|
+
from __future__ import annotations
|
|
60
|
+
|
|
61
|
+
import asyncio
|
|
62
|
+
import time
|
|
63
|
+
from collections.abc import Mapping
|
|
64
|
+
from typing import Any, Final, Protocol
|
|
65
|
+
|
|
66
|
+
from .config import SYNC_FALLBACK_WAIT_SECONDS
|
|
67
|
+
from .errors import (
|
|
68
|
+
CompletionFailedError,
|
|
69
|
+
ResultUnavailableError,
|
|
70
|
+
SyncFallbackWaitExhaustedError,
|
|
71
|
+
refusal_from_answer,
|
|
72
|
+
translated_failures,
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
__all__ = ["InvocationMixin", "is_sync_timeout_fallback"]
|
|
76
|
+
|
|
77
|
+
#: How often the client asks the queue for the status of a followed job, in
|
|
78
|
+
#: seconds.
|
|
79
|
+
_FALLBACK_POLL_SECONDS: Final[float] = 1.0
|
|
80
|
+
|
|
81
|
+
#: Job-status values that end a job without a usable result.
|
|
82
|
+
_FALLBACK_FAILED_STATUSES: Final[frozenset[str]] = frozenset(
|
|
83
|
+
{"failed", "error", "timeout", "stopped", "cancelled"}
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
#: Job-status values that end a job with its result.
|
|
87
|
+
_FALLBACK_DONE_STATUSES: Final[frozenset[str]] = frozenset({"completed"})
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def is_sync_timeout_fallback(answer: object) -> bool:
|
|
91
|
+
"""
|
|
92
|
+
Whether ``answer`` is the service's sync-timeout queue fallback: the
|
|
93
|
+
acknowledgement of a command that overran the synchronous cap, carrying a job
|
|
94
|
+
identity and ``queued_after_timeout`` true instead of the command's result.
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(answer, Mapping):
|
|
97
|
+
return False
|
|
98
|
+
job_id = answer.get("job_id")
|
|
99
|
+
return (
|
|
100
|
+
answer.get("queued_after_timeout") is True
|
|
101
|
+
and isinstance(job_id, str)
|
|
102
|
+
and bool(job_id.strip())
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _job_answer(outcome: object) -> dict[str, Any] | None:
|
|
107
|
+
"""
|
|
108
|
+
The command's own answer inside a finished job's ``result``: the envelope the
|
|
109
|
+
job's command returned (``result.result``), or the result itself when it is
|
|
110
|
+
already an envelope. ``None`` when the job carries none.
|
|
111
|
+
"""
|
|
112
|
+
if not isinstance(outcome, Mapping):
|
|
113
|
+
return None
|
|
114
|
+
inner = outcome.get("result")
|
|
115
|
+
if isinstance(inner, Mapping):
|
|
116
|
+
return dict(inner)
|
|
117
|
+
if "success" in outcome:
|
|
118
|
+
return dict(outcome)
|
|
119
|
+
return None
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class _FrameworkClientProtocol(Protocol):
|
|
123
|
+
"""Protocol for the type of the held framework client instance.
|
|
124
|
+
|
|
125
|
+
Defines the minimal interface this mixin expects from the held _client
|
|
126
|
+
attribute, allowing strict type checking without importing the concrete
|
|
127
|
+
JsonRpcClient class.
|
|
128
|
+
"""
|
|
129
|
+
|
|
130
|
+
async def execute_command_unified(
|
|
131
|
+
self,
|
|
132
|
+
command: str,
|
|
133
|
+
params: dict[str, Any] | None = None,
|
|
134
|
+
*,
|
|
135
|
+
expect_queue: bool | None = None,
|
|
136
|
+
auto_poll: bool = True,
|
|
137
|
+
timeout: float | None = None,
|
|
138
|
+
) -> Any:
|
|
139
|
+
"""Execute a command, transparently handling queued or immediate responses.
|
|
140
|
+
|
|
141
|
+
When the service answers by queueing, this method waits for the job to
|
|
142
|
+
reach a terminal state via the framework's WebSocket facilities and
|
|
143
|
+
returns the finished answer. When the service answers directly, it
|
|
144
|
+
returns the answer immediately. To the caller, both paths are
|
|
145
|
+
indistinguishable: the return value in both cases is the finished
|
|
146
|
+
command result.
|
|
147
|
+
|
|
148
|
+
Args:
|
|
149
|
+
command: Name of the command to execute.
|
|
150
|
+
params: Optional mapping of command parameters.
|
|
151
|
+
auto_poll: When True (default), wait for queued jobs to completion.
|
|
152
|
+
timeout: Maximum seconds to wait for a queued job. Defaults to
|
|
153
|
+
the framework's configured timeout.
|
|
154
|
+
|
|
155
|
+
Returns:
|
|
156
|
+
A dictionary with "mode" and "result" keys. The "mode" field
|
|
157
|
+
indicates whether the response was "immediate" or "queued"; the
|
|
158
|
+
"result" field holds the finished command result.
|
|
159
|
+
"""
|
|
160
|
+
...
|
|
161
|
+
|
|
162
|
+
async def get_job_status(
|
|
163
|
+
self,
|
|
164
|
+
job_id: str,
|
|
165
|
+
*,
|
|
166
|
+
sync_from_queue: bool = False,
|
|
167
|
+
) -> Any:
|
|
168
|
+
"""Return the status snapshot for a job.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
job_id: The job identifier.
|
|
172
|
+
sync_from_queue: When True, fetch status from the server queue
|
|
173
|
+
and merge it with locally recorded events.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
A job status snapshot with normalized fields.
|
|
177
|
+
"""
|
|
178
|
+
...
|
|
179
|
+
|
|
180
|
+
async def get_job_messages(
|
|
181
|
+
self,
|
|
182
|
+
job_id: str,
|
|
183
|
+
*,
|
|
184
|
+
limit: int | None = None,
|
|
185
|
+
) -> list[dict[str, Any]]:
|
|
186
|
+
"""Return the progress messages recorded for a job.
|
|
187
|
+
|
|
188
|
+
Args:
|
|
189
|
+
job_id: The job identifier.
|
|
190
|
+
limit: Maximum number of recent messages to return.
|
|
191
|
+
|
|
192
|
+
Returns:
|
|
193
|
+
A list of message dictionaries (oldest first).
|
|
194
|
+
"""
|
|
195
|
+
...
|
|
196
|
+
|
|
197
|
+
async def queue_get_job_status(self, job_id: str) -> dict[str, Any]:
|
|
198
|
+
"""Return the raw ``queue_get_job_status`` answer of the service.
|
|
199
|
+
|
|
200
|
+
Args:
|
|
201
|
+
job_id: The job identifier.
|
|
202
|
+
|
|
203
|
+
Returns:
|
|
204
|
+
The queue's answer: ``{"success": ..., "data": {"status": ...,
|
|
205
|
+
"result": ...}}``.
|
|
206
|
+
"""
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
async def cancel_command(
|
|
210
|
+
self,
|
|
211
|
+
job_id: str,
|
|
212
|
+
) -> dict[str, Any]:
|
|
213
|
+
"""Cancel a queued command execution.
|
|
214
|
+
|
|
215
|
+
Args:
|
|
216
|
+
job_id: The job identifier.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
The result of the cancellation operation.
|
|
220
|
+
"""
|
|
221
|
+
...
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class InvocationMixin:
|
|
225
|
+
"""Mixin providing the single invocation seam for the ai-logger client.
|
|
226
|
+
|
|
227
|
+
Declares the private invocation entry point through which all typed methods
|
|
228
|
+
reach the service, and the public generic call for commands not yet named.
|
|
229
|
+
|
|
230
|
+
The held _client attribute (an instance of the adapter framework's
|
|
231
|
+
JsonRpcClient) is accessed only through the private invocation entry point,
|
|
232
|
+
so the service is reached through exactly one place in the client codebase.
|
|
233
|
+
"""
|
|
234
|
+
|
|
235
|
+
# _client is declared here for type annotation purposes only.
|
|
236
|
+
# At runtime, _client is set by AiLoggerClient.__init__ to an instance
|
|
237
|
+
# of mcp_proxy_adapter.client.jsonrpc_client.JsonRpcClient.
|
|
238
|
+
_client: _FrameworkClientProtocol
|
|
239
|
+
|
|
240
|
+
# How long a command that overran the service's sync cap is followed to the
|
|
241
|
+
# end of its queued job; AiLoggerClient.__init__ replaces it with the
|
|
242
|
+
# configured ``client.sync_fallback_wait_seconds``.
|
|
243
|
+
_sync_fallback_wait_seconds: float = SYNC_FALLBACK_WAIT_SECONDS
|
|
244
|
+
|
|
245
|
+
async def _invoke(
|
|
246
|
+
self,
|
|
247
|
+
command: str,
|
|
248
|
+
params: dict[str, Any] | None = None,
|
|
249
|
+
*,
|
|
250
|
+
wait_for_result: bool = True,
|
|
251
|
+
) -> Any:
|
|
252
|
+
"""Private invocation entry point.
|
|
253
|
+
|
|
254
|
+
Forwards the command name and parameters to the held framework
|
|
255
|
+
client's execute_command_unified method with ``expect_queue=False``.
|
|
256
|
+
Every ai-logger command is declared direct, so this selects the
|
|
257
|
+
adapter's HTTP JSON-RPC path and avoids opening a WebSocket command
|
|
258
|
+
session before the request. The caller receives the direct command
|
|
259
|
+
result without any job identity or status.
|
|
260
|
+
|
|
261
|
+
The ``wait_for_result`` argument remains part of the public client
|
|
262
|
+
surface for compatibility, but direct commands do not queue and return
|
|
263
|
+
their result on either path.
|
|
264
|
+
|
|
265
|
+
This is the single seam through which every typed method of the
|
|
266
|
+
client reaches the service.
|
|
267
|
+
|
|
268
|
+
Every call to the framework client is wrapped in the reusable
|
|
269
|
+
translation guard (A-007): a ClientError from the framework is
|
|
270
|
+
translated to this client's vocabulary and raised with the original
|
|
271
|
+
as its cause. Plain programming errors propagate unchanged.
|
|
272
|
+
|
|
273
|
+
SYNC-TIMEOUT FALLBACK. A command that overruns the service's sync cap
|
|
274
|
+
is answered with the acknowledgement of a detached job (``job_id``,
|
|
275
|
+
``queued_after_timeout`` true) instead of its result. The client then
|
|
276
|
+
follows that job with ``queue_get_job_status`` until it ends, within
|
|
277
|
+
``client.sync_fallback_wait_seconds``, and continues with the answer
|
|
278
|
+
the job finished with, so the caller sees what an inline call returns.
|
|
279
|
+
The command is dispatched once. When the wait runs out,
|
|
280
|
+
``SyncFallbackWaitExhaustedError`` (with the ``job_id``) is raised.
|
|
281
|
+
|
|
282
|
+
The answer-borne refusal recogniser is applied exactly once, on the
|
|
283
|
+
completed answer (the default wait_for_result=True path), immediately
|
|
284
|
+
before the return (A-007): a refusal carried in the answer dict is
|
|
285
|
+
recognised and converted to ServiceRefusalError, matching the path
|
|
286
|
+
where a refusal is raised as a translated framework failure.
|
|
287
|
+
|
|
288
|
+
Args:
|
|
289
|
+
command: The name of the command to execute.
|
|
290
|
+
params: An optional mapping of command parameters. If None, passed
|
|
291
|
+
as None to the framework client, which treats it as an empty
|
|
292
|
+
mapping.
|
|
293
|
+
wait_for_result: Retained for the public client API. ai-logger
|
|
294
|
+
commands are direct, so it does not change their result.
|
|
295
|
+
|
|
296
|
+
Returns:
|
|
297
|
+
The direct result returned by the service.
|
|
298
|
+
|
|
299
|
+
Raises:
|
|
300
|
+
ServiceRefusalError: When the service or transport indicates refusal.
|
|
301
|
+
ServiceUnreachableError: When the service cannot be reached.
|
|
302
|
+
Any error subclassing AiLoggerClientError: From other failures of
|
|
303
|
+
the service or transport, translated from framework ClientError.
|
|
304
|
+
RuntimeError or ValueError: Propagated unchanged if raised by the
|
|
305
|
+
client itself or the caller's code.
|
|
306
|
+
"""
|
|
307
|
+
result = await self._invoke_unconverted(command, params, wait_for_result)
|
|
308
|
+
|
|
309
|
+
# A command that overran the service's sync cap is answered with the
|
|
310
|
+
# acknowledgement of a detached job instead of its result: follow that
|
|
311
|
+
# job (the command is not dispatched again) and carry on with the
|
|
312
|
+
# command's own answer exactly as if it had been given inline.
|
|
313
|
+
if is_sync_timeout_fallback(result):
|
|
314
|
+
result = await self._follow_sync_fallback(result, command)
|
|
315
|
+
|
|
316
|
+
# Apply the refusal recogniser exactly once, on the completed answer.
|
|
317
|
+
# Both the raised route (framework ClientRequestError translated to
|
|
318
|
+
# ServiceRefusalError) and the answer-borne route (refusal dict
|
|
319
|
+
# carrying framework_code) end in the same kind.
|
|
320
|
+
refusal = refusal_from_answer(result, command)
|
|
321
|
+
if refusal is not None:
|
|
322
|
+
raise refusal
|
|
323
|
+
|
|
324
|
+
return result
|
|
325
|
+
|
|
326
|
+
async def _follow_sync_fallback(
|
|
327
|
+
self, acknowledgement: Mapping[str, Any], command: str
|
|
328
|
+
) -> Any:
|
|
329
|
+
"""The command's own answer, taken from the job a sync timeout queued.
|
|
330
|
+
|
|
331
|
+
The service answered ``command`` with the sync-timeout acknowledgement:
|
|
332
|
+
the command was dispatched once and runs on as a job. Poll that job
|
|
333
|
+
through ``queue_get_job_status`` until it ends and return the answer it
|
|
334
|
+
finished with - the command's ``{success, data}`` envelope, from
|
|
335
|
+
``data.result.result`` of the status answer. A refusing envelope is
|
|
336
|
+
returned as it is, so the caller's refusal recogniser turns it into the
|
|
337
|
+
same ``ServiceRefusalError`` the inline answer would have raised.
|
|
338
|
+
|
|
339
|
+
Raises:
|
|
340
|
+
SyncFallbackWaitExhaustedError: When the job does not end within
|
|
341
|
+
``client.sync_fallback_wait_seconds``; carries the job id.
|
|
342
|
+
CompletionFailedError: When the job ends failed without an answer.
|
|
343
|
+
ResultUnavailableError: When the job ends without an answer.
|
|
344
|
+
"""
|
|
345
|
+
job_id = str(acknowledgement["job_id"]).strip()
|
|
346
|
+
limit = self._sync_fallback_wait_seconds
|
|
347
|
+
deadline = time.monotonic() + limit
|
|
348
|
+
while True:
|
|
349
|
+
with translated_failures():
|
|
350
|
+
raw = await self._client.queue_get_job_status(job_id)
|
|
351
|
+
data = raw.get("data") if isinstance(raw, Mapping) else None
|
|
352
|
+
status = ""
|
|
353
|
+
if isinstance(data, Mapping):
|
|
354
|
+
status = str(data.get("status") or "").lower()
|
|
355
|
+
if status in _FALLBACK_DONE_STATUSES or status in _FALLBACK_FAILED_STATUSES:
|
|
356
|
+
assert isinstance(data, Mapping)
|
|
357
|
+
answer = _job_answer(data.get("result"))
|
|
358
|
+
if answer is not None:
|
|
359
|
+
return answer
|
|
360
|
+
if status in _FALLBACK_FAILED_STATUSES:
|
|
361
|
+
raise CompletionFailedError(
|
|
362
|
+
f"{command} job {job_id} ended {status!r} without an answer"
|
|
363
|
+
)
|
|
364
|
+
raise ResultUnavailableError(
|
|
365
|
+
f"{command} job {job_id} ended {status!r} without an answer"
|
|
366
|
+
)
|
|
367
|
+
remaining = deadline - time.monotonic()
|
|
368
|
+
if remaining <= 0:
|
|
369
|
+
raise SyncFallbackWaitExhaustedError(
|
|
370
|
+
f"{command} exceeded the service's sync timeout and its job "
|
|
371
|
+
f"{job_id} did not finish within {limit:g} seconds "
|
|
372
|
+
"(client.sync_fallback_wait_seconds)",
|
|
373
|
+
job_id,
|
|
374
|
+
)
|
|
375
|
+
await asyncio.sleep(min(_FALLBACK_POLL_SECONDS, remaining))
|
|
376
|
+
|
|
377
|
+
async def _invoke_unconverted(
|
|
378
|
+
self,
|
|
379
|
+
command: str,
|
|
380
|
+
params: dict[str, Any] | None,
|
|
381
|
+
wait_for_result: bool,
|
|
382
|
+
) -> Any:
|
|
383
|
+
"""The answer of one command as the service gave it, refusals unconverted.
|
|
384
|
+
|
|
385
|
+
Everything ``_invoke`` does except recognising an answer-borne refusal:
|
|
386
|
+
the request goes out through the translation guard (a framework failure
|
|
387
|
+
arrives as a typed error of this client) and the command's answer comes
|
|
388
|
+
back as it was, a refusal answer included. Used by the observation
|
|
389
|
+
methods that classify an answer rather than act on it.
|
|
390
|
+
"""
|
|
391
|
+
with translated_failures():
|
|
392
|
+
response: dict[str, Any] = await self._client.execute_command_unified(
|
|
393
|
+
command,
|
|
394
|
+
params,
|
|
395
|
+
expect_queue=False,
|
|
396
|
+
auto_poll=wait_for_result,
|
|
397
|
+
)
|
|
398
|
+
|
|
399
|
+
# The direct execution path returns the command answer in ``result``.
|
|
400
|
+
return response.get("result")
|
|
401
|
+
|
|
402
|
+
async def call(
|
|
403
|
+
self,
|
|
404
|
+
command: str,
|
|
405
|
+
params: dict[str, Any] | None = None,
|
|
406
|
+
*,
|
|
407
|
+
wait_for_result: bool = True,
|
|
408
|
+
) -> Any:
|
|
409
|
+
"""Execute any command the service publishes.
|
|
410
|
+
|
|
411
|
+
A generic invocation interface for commands not yet named in this
|
|
412
|
+
client. Reaches the service through the private invocation entry
|
|
413
|
+
point. The command name and parameters are carried unchanged.
|
|
414
|
+
|
|
415
|
+
This is an escape hatch, allowing an already-installed client to
|
|
416
|
+
reach commands published after the client was released, without
|
|
417
|
+
waiting for a new client release. The client-surface-agreement check
|
|
418
|
+
owned by the plan counts this method on neither side of its comparison,
|
|
419
|
+
as it is not a typed method of any command and does not commit the
|
|
420
|
+
client to any specific command name or contract.
|
|
421
|
+
|
|
422
|
+
Args:
|
|
423
|
+
command: The name of the command to execute. The service need not
|
|
424
|
+
have published a typed method for this command; any command
|
|
425
|
+
the service publishes can be reached.
|
|
426
|
+
params: An optional mapping of command parameters.
|
|
427
|
+
wait_for_result: When True (default), wait for queued commands to
|
|
428
|
+
completion and return the finished result. When False, return
|
|
429
|
+
the job identity if queued, or the direct result if immediate.
|
|
430
|
+
|
|
431
|
+
Returns:
|
|
432
|
+
When wait_for_result is True: the result returned by the service.
|
|
433
|
+
|
|
434
|
+
When wait_for_result is False: the job identity envelope if the
|
|
435
|
+
command was queued, or the direct result if immediate.
|
|
436
|
+
"""
|
|
437
|
+
return await self._invoke(command, params, wait_for_result=wait_for_result)
|
|
438
|
+
|
|
439
|
+
async def get_job_status(
|
|
440
|
+
self,
|
|
441
|
+
job_id: str,
|
|
442
|
+
*,
|
|
443
|
+
sync_from_queue: bool = False,
|
|
444
|
+
) -> Any:
|
|
445
|
+
"""Get the status of a queued command.
|
|
446
|
+
|
|
447
|
+
Delegates to the framework client's job-status observation surface to
|
|
448
|
+
return the current snapshot of a job's execution state. This method
|
|
449
|
+
is not a command of the service; it is an observation surface over
|
|
450
|
+
the framework's own job-tracking API and is not counted in the
|
|
451
|
+
client-surface-agreement comparison.
|
|
452
|
+
|
|
453
|
+
The call to the framework client is wrapped in the reusable
|
|
454
|
+
translation guard (A-007): a ClientError from the framework is
|
|
455
|
+
translated to this client's vocabulary and raised with the original
|
|
456
|
+
as its cause.
|
|
457
|
+
|
|
458
|
+
Args:
|
|
459
|
+
job_id: The job identifier, typically returned by a non-blocking
|
|
460
|
+
command execution (when wait_for_result=False).
|
|
461
|
+
sync_from_queue: When True, fetch the latest status from the
|
|
462
|
+
server queue and merge it with locally recorded events. When
|
|
463
|
+
False (default), use only events recorded locally.
|
|
464
|
+
|
|
465
|
+
Returns:
|
|
466
|
+
A job status snapshot with normalized fields from the framework
|
|
467
|
+
client's tracking API.
|
|
468
|
+
|
|
469
|
+
Raises:
|
|
470
|
+
Any error subclassing AiLoggerClientError: From failures of
|
|
471
|
+
the transport, translated from framework ClientError.
|
|
472
|
+
RuntimeError or ValueError: Propagated unchanged if raised by the
|
|
473
|
+
client itself.
|
|
474
|
+
"""
|
|
475
|
+
with translated_failures():
|
|
476
|
+
return await self._client.get_job_status(
|
|
477
|
+
job_id, sync_from_queue=sync_from_queue
|
|
478
|
+
)
|
|
479
|
+
|
|
480
|
+
async def get_job_messages(
|
|
481
|
+
self,
|
|
482
|
+
job_id: str,
|
|
483
|
+
*,
|
|
484
|
+
limit: int | None = None,
|
|
485
|
+
) -> list[dict[str, Any]]:
|
|
486
|
+
"""Get progress messages for a queued command.
|
|
487
|
+
|
|
488
|
+
Delegates to the framework client's job-message observation surface to
|
|
489
|
+
return the bounded recent history of progress messages emitted by a
|
|
490
|
+
job. This method is not a command of the service; it is an observation
|
|
491
|
+
surface over the framework's own job-tracking API and is not counted
|
|
492
|
+
in the client-surface-agreement comparison.
|
|
493
|
+
|
|
494
|
+
The call to the framework client is wrapped in the reusable
|
|
495
|
+
translation guard (A-007): a ClientError from the framework is
|
|
496
|
+
translated to this client's vocabulary and raised with the original
|
|
497
|
+
as its cause.
|
|
498
|
+
|
|
499
|
+
Args:
|
|
500
|
+
job_id: The job identifier, typically returned by a non-blocking
|
|
501
|
+
command execution (when wait_for_result=False).
|
|
502
|
+
limit: Maximum number of recent messages to return. If None, the
|
|
503
|
+
framework client determines the default limit.
|
|
504
|
+
|
|
505
|
+
Returns:
|
|
506
|
+
A list of message dictionaries in chronological order (oldest
|
|
507
|
+
first), each containing the fields published by the service or
|
|
508
|
+
framework for that message.
|
|
509
|
+
|
|
510
|
+
Raises:
|
|
511
|
+
Any error subclassing AiLoggerClientError: From failures of
|
|
512
|
+
the transport, translated from framework ClientError.
|
|
513
|
+
RuntimeError or ValueError: Propagated unchanged if raised by the
|
|
514
|
+
client itself.
|
|
515
|
+
"""
|
|
516
|
+
with translated_failures():
|
|
517
|
+
return await self._client.get_job_messages(job_id, limit=limit)
|
|
518
|
+
|
|
519
|
+
async def cancel_command(
|
|
520
|
+
self,
|
|
521
|
+
job_id: str,
|
|
522
|
+
) -> dict[str, Any]:
|
|
523
|
+
"""Cancel a queued command execution.
|
|
524
|
+
|
|
525
|
+
Delegates to the framework client's cancellation surface to stop and
|
|
526
|
+
remove a queued job. This method is not a command of the service; it
|
|
527
|
+
is an observation surface over the framework's own job-tracking API
|
|
528
|
+
and is not counted in the client-surface-agreement comparison.
|
|
529
|
+
|
|
530
|
+
The call to the framework client is wrapped in the reusable
|
|
531
|
+
translation guard (A-007): a ClientError from the framework is
|
|
532
|
+
translated to this client's vocabulary and raised with the original
|
|
533
|
+
as its cause.
|
|
534
|
+
|
|
535
|
+
Args:
|
|
536
|
+
job_id: The job identifier, typically returned by a non-blocking
|
|
537
|
+
command execution (when wait_for_result=False).
|
|
538
|
+
|
|
539
|
+
Returns:
|
|
540
|
+
The result dictionary from the framework client's cancellation
|
|
541
|
+
operation.
|
|
542
|
+
|
|
543
|
+
Raises:
|
|
544
|
+
Any error subclassing AiLoggerClientError: From failures of
|
|
545
|
+
the transport, translated from framework ClientError.
|
|
546
|
+
RuntimeError or ValueError: Propagated unchanged if raised by the
|
|
547
|
+
client itself.
|
|
548
|
+
"""
|
|
549
|
+
with translated_failures():
|
|
550
|
+
return await self._client.cancel_command(job_id)
|