necoherence-http-adapter 0.2.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.
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
# SPDX-FileCopyrightText: 2026 barineco
|
|
2
|
+
# SPDX-License-Identifier: MIT
|
|
3
|
+
|
|
4
|
+
"""Finite HTTP host adapter for the pure network-effect ledger."""
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
import http.client
|
|
9
|
+
import queue
|
|
10
|
+
import socket
|
|
11
|
+
import threading
|
|
12
|
+
import time
|
|
13
|
+
from typing import Protocol, cast
|
|
14
|
+
from urllib.parse import SplitResult, urlsplit
|
|
15
|
+
|
|
16
|
+
from necoherence import network_effect as ne
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class MonotonicClockPort(Protocol):
|
|
20
|
+
def capability(self) -> ne.MonotonicClockCapability: ...
|
|
21
|
+
def observe(self) -> ne.MonotonicClockObservation: ...
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class CancellationPort(Protocol):
|
|
25
|
+
def capability(self) -> ne.CancellationCapability: ...
|
|
26
|
+
def request(
|
|
27
|
+
self,
|
|
28
|
+
capability: ne.CancellationCapability,
|
|
29
|
+
handle: ne.ExecutionHandle,
|
|
30
|
+
) -> bool: ...
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SystemMonotonicClock:
|
|
34
|
+
def __init__(
|
|
35
|
+
self,
|
|
36
|
+
capability: ne.MonotonicClockCapability | None = None,
|
|
37
|
+
) -> None:
|
|
38
|
+
self._capability = capability or ne.MonotonicClockCapability(
|
|
39
|
+
ne.OpaqueEvidence("python-system-monotonic-clock"),
|
|
40
|
+
ne.ClockBasis("python-monotonic"),
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
def capability(self) -> ne.MonotonicClockCapability:
|
|
44
|
+
return self._capability
|
|
45
|
+
|
|
46
|
+
def observe(self) -> ne.MonotonicClockObservation:
|
|
47
|
+
return ne.MonotonicClockObservation(
|
|
48
|
+
self._capability.clock_basis,
|
|
49
|
+
time.monotonic_ns(),
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
class EventCancellationPort:
|
|
54
|
+
def __init__(
|
|
55
|
+
self,
|
|
56
|
+
capability: ne.CancellationCapability | None = None,
|
|
57
|
+
) -> None:
|
|
58
|
+
self._capability = capability or ne.CancellationCapability(
|
|
59
|
+
ne.OpaqueEvidence("python-event-cancellation")
|
|
60
|
+
)
|
|
61
|
+
self._events: dict[ne.ExecutionHandle, threading.Event] = {}
|
|
62
|
+
self._lock = threading.Lock()
|
|
63
|
+
|
|
64
|
+
def capability(self) -> ne.CancellationCapability:
|
|
65
|
+
return self._capability
|
|
66
|
+
|
|
67
|
+
def register(self, handle: ne.ExecutionHandle) -> None:
|
|
68
|
+
with self._lock:
|
|
69
|
+
self._events[handle] = threading.Event()
|
|
70
|
+
|
|
71
|
+
def request(
|
|
72
|
+
self,
|
|
73
|
+
capability: ne.CancellationCapability,
|
|
74
|
+
handle: ne.ExecutionHandle,
|
|
75
|
+
) -> bool:
|
|
76
|
+
if capability != self._capability:
|
|
77
|
+
return False
|
|
78
|
+
with self._lock:
|
|
79
|
+
event = self._events.get(handle)
|
|
80
|
+
if event is None:
|
|
81
|
+
return False
|
|
82
|
+
event.set()
|
|
83
|
+
return True
|
|
84
|
+
|
|
85
|
+
def cancelled(self, handle: ne.ExecutionHandle) -> bool:
|
|
86
|
+
with self._lock:
|
|
87
|
+
event = self._events.get(handle)
|
|
88
|
+
return event is not None and event.is_set()
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
@dataclass(slots=True)
|
|
92
|
+
class _Worker:
|
|
93
|
+
command: ne.SendCommand
|
|
94
|
+
queue: queue.SimpleQueue[ne.AdapterObservation]
|
|
95
|
+
cancel: threading.Event
|
|
96
|
+
next_source: int = 1
|
|
97
|
+
delivery: int = 0
|
|
98
|
+
deadline_emitted: bool = False
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class HttpHostAdapter:
|
|
102
|
+
def __init__(
|
|
103
|
+
self,
|
|
104
|
+
clock: MonotonicClockPort | None = None,
|
|
105
|
+
cancellation: CancellationPort | None = None,
|
|
106
|
+
) -> None:
|
|
107
|
+
self._clock = clock
|
|
108
|
+
self._cancellation = cancellation or EventCancellationPort()
|
|
109
|
+
self._workers: dict[ne.ExecutionHandle, _Worker] = {}
|
|
110
|
+
self._counter = 0
|
|
111
|
+
self._lock = threading.Lock()
|
|
112
|
+
|
|
113
|
+
def start(self, command: ne.SendCommand) -> ne.Started | ne.StartFailed:
|
|
114
|
+
input = command.prepared.input
|
|
115
|
+
failure = self._start_failure(input)
|
|
116
|
+
if failure is not None:
|
|
117
|
+
return self._start_failed(input, failure)
|
|
118
|
+
with self._lock:
|
|
119
|
+
handle = ne.ExecutionHandle(f"python-http-{self._counter}")
|
|
120
|
+
self._counter += 1
|
|
121
|
+
worker = _Worker(command, queue.SimpleQueue(), threading.Event())
|
|
122
|
+
self._workers[handle] = worker
|
|
123
|
+
register = getattr(self._cancellation, "register", None)
|
|
124
|
+
if callable(register):
|
|
125
|
+
register(handle)
|
|
126
|
+
try:
|
|
127
|
+
thread = threading.Thread(
|
|
128
|
+
target=self._run,
|
|
129
|
+
args=(handle, worker, 0),
|
|
130
|
+
daemon=True,
|
|
131
|
+
)
|
|
132
|
+
thread.start()
|
|
133
|
+
except BaseException:
|
|
134
|
+
with self._lock:
|
|
135
|
+
self._workers.pop(handle, None)
|
|
136
|
+
return self._start_failed(input, ne.ExecutionStartFailed())
|
|
137
|
+
return ne.Started(input.attempt, handle)
|
|
138
|
+
|
|
139
|
+
def observe(self, started: ne.StartedAttempt) -> ne.HostObservation:
|
|
140
|
+
worker = self._worker(started.handle, started.prepared.input.attempt)
|
|
141
|
+
if worker is None:
|
|
142
|
+
return ne.PendingObservation(
|
|
143
|
+
started.prepared.input.attempt,
|
|
144
|
+
started.handle,
|
|
145
|
+
)
|
|
146
|
+
observation = self._take(worker)
|
|
147
|
+
if observation is not None:
|
|
148
|
+
return self._delivered(worker, observation)
|
|
149
|
+
self._enqueue_deadline(started.handle, worker)
|
|
150
|
+
observation = self._take(worker)
|
|
151
|
+
if observation is not None:
|
|
152
|
+
return self._delivered(worker, observation)
|
|
153
|
+
return ne.PendingObservation(
|
|
154
|
+
worker.command.prepared.input.attempt,
|
|
155
|
+
started.handle,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
def cancel(self, command: ne.CancelCommand) -> ne.HostObservation:
|
|
159
|
+
worker = self._worker(command.handle, command.attempt)
|
|
160
|
+
if worker is None:
|
|
161
|
+
return ne.PendingObservation(command.attempt, command.handle)
|
|
162
|
+
observation = self._take(worker)
|
|
163
|
+
if observation is not None:
|
|
164
|
+
return self._delivered(worker, observation)
|
|
165
|
+
source = self._reserve_source(worker)
|
|
166
|
+
if self._cancellation.request(command.capability, command.handle):
|
|
167
|
+
worker.cancel.set()
|
|
168
|
+
observation = ne.CancellationObservation(
|
|
169
|
+
command.attempt,
|
|
170
|
+
command.handle,
|
|
171
|
+
source,
|
|
172
|
+
ne.Unknown(ne.UnknownEffectReason.CANCELLATION),
|
|
173
|
+
)
|
|
174
|
+
else:
|
|
175
|
+
observation = self._failure_observation(
|
|
176
|
+
worker.command.prepared.input,
|
|
177
|
+
command.handle,
|
|
178
|
+
source,
|
|
179
|
+
ne.HostStage.CANCEL,
|
|
180
|
+
ne.CancellationRejected(),
|
|
181
|
+
)
|
|
182
|
+
return self._delivered(worker, observation)
|
|
183
|
+
|
|
184
|
+
def close(self, started: ne.StartedAttempt) -> ne.HostObservation:
|
|
185
|
+
worker = self._worker(
|
|
186
|
+
started.handle,
|
|
187
|
+
started.prepared.input.attempt,
|
|
188
|
+
)
|
|
189
|
+
if worker is None:
|
|
190
|
+
return ne.PendingObservation(
|
|
191
|
+
started.prepared.input.attempt,
|
|
192
|
+
started.handle,
|
|
193
|
+
)
|
|
194
|
+
source = self._reserve_source(worker)
|
|
195
|
+
observation = self._failure_observation(
|
|
196
|
+
worker.command.prepared.input,
|
|
197
|
+
started.handle,
|
|
198
|
+
source,
|
|
199
|
+
ne.HostStage.RESPONSE_BODY,
|
|
200
|
+
ne.HostClosed(),
|
|
201
|
+
)
|
|
202
|
+
return self._delivered(worker, observation)
|
|
203
|
+
|
|
204
|
+
def _start_failure(self, input: ne.AttemptInput) -> ne.HostFailure | None:
|
|
205
|
+
target = urlsplit(input.instruction.target)
|
|
206
|
+
if target.scheme not in {"http", "https"}:
|
|
207
|
+
return ne.UnsupportedUriScheme()
|
|
208
|
+
if not ne.valid_attempt_input(input):
|
|
209
|
+
return ne.InvalidHostConfiguration()
|
|
210
|
+
if input.deadline is not None:
|
|
211
|
+
if (
|
|
212
|
+
self._clock is None
|
|
213
|
+
or self._clock.capability() != input.deadline.capability
|
|
214
|
+
or input.configuration.monotonic_clock
|
|
215
|
+
!= input.deadline.capability
|
|
216
|
+
):
|
|
217
|
+
return ne.DeadlineControlUnavailable()
|
|
218
|
+
if input.configuration.cancellation is not None:
|
|
219
|
+
capability = getattr(self._cancellation, "capability", None)
|
|
220
|
+
if (
|
|
221
|
+
not callable(capability)
|
|
222
|
+
or capability() != input.configuration.cancellation
|
|
223
|
+
):
|
|
224
|
+
return ne.InvalidHostConfiguration()
|
|
225
|
+
return None
|
|
226
|
+
|
|
227
|
+
def _run(
|
|
228
|
+
self,
|
|
229
|
+
handle: ne.ExecutionHandle,
|
|
230
|
+
worker: _Worker,
|
|
231
|
+
source: int,
|
|
232
|
+
) -> None:
|
|
233
|
+
input = worker.command.prepared.input
|
|
234
|
+
try:
|
|
235
|
+
deadline = self._deadline_observation(
|
|
236
|
+
input,
|
|
237
|
+
handle,
|
|
238
|
+
source,
|
|
239
|
+
)
|
|
240
|
+
outcome = (
|
|
241
|
+
deadline
|
|
242
|
+
if deadline is not None
|
|
243
|
+
else self._request(handle, input, source)
|
|
244
|
+
)
|
|
245
|
+
except BaseException as error:
|
|
246
|
+
stage, cause = self._classify_exception(error)
|
|
247
|
+
outcome = self._failure_observation(
|
|
248
|
+
input,
|
|
249
|
+
handle,
|
|
250
|
+
source,
|
|
251
|
+
stage,
|
|
252
|
+
cause,
|
|
253
|
+
)
|
|
254
|
+
worker.queue.put(outcome)
|
|
255
|
+
|
|
256
|
+
def _request(
|
|
257
|
+
self,
|
|
258
|
+
handle: ne.ExecutionHandle,
|
|
259
|
+
input: ne.AttemptInput,
|
|
260
|
+
source: int,
|
|
261
|
+
) -> ne.AdapterObservation:
|
|
262
|
+
instruction = input.instruction
|
|
263
|
+
target = urlsplit(instruction.target)
|
|
264
|
+
try:
|
|
265
|
+
headers = tuple(
|
|
266
|
+
self._encode_request_header(header)
|
|
267
|
+
for header in instruction.headers
|
|
268
|
+
)
|
|
269
|
+
except (UnicodeError, ValueError):
|
|
270
|
+
return self._failure_observation(
|
|
271
|
+
input,
|
|
272
|
+
handle,
|
|
273
|
+
source,
|
|
274
|
+
ne.HostStage.REQUEST_WRITE,
|
|
275
|
+
ne.RequestHeaderEncodingUnavailable(),
|
|
276
|
+
)
|
|
277
|
+
connection = self._new_connection(target)
|
|
278
|
+
try:
|
|
279
|
+
try:
|
|
280
|
+
connection.connect()
|
|
281
|
+
except socket.gaierror:
|
|
282
|
+
return self._failure_observation(
|
|
283
|
+
input,
|
|
284
|
+
handle,
|
|
285
|
+
source,
|
|
286
|
+
ne.HostStage.REQUEST_WRITE,
|
|
287
|
+
ne.NameResolutionFailed(),
|
|
288
|
+
)
|
|
289
|
+
except (ConnectionError, OSError):
|
|
290
|
+
return self._failure_observation(
|
|
291
|
+
input,
|
|
292
|
+
handle,
|
|
293
|
+
source,
|
|
294
|
+
ne.HostStage.REQUEST_WRITE,
|
|
295
|
+
ne.ConnectionFailed(),
|
|
296
|
+
)
|
|
297
|
+
try:
|
|
298
|
+
path = target.path or "/"
|
|
299
|
+
if target.query:
|
|
300
|
+
path += "?" + target.query
|
|
301
|
+
connection.putrequest(
|
|
302
|
+
instruction.method,
|
|
303
|
+
path,
|
|
304
|
+
skip_host=True,
|
|
305
|
+
skip_accept_encoding=True,
|
|
306
|
+
)
|
|
307
|
+
for name, value in headers:
|
|
308
|
+
connection.putheader(name, value)
|
|
309
|
+
connection.endheaders(bytes(instruction.body))
|
|
310
|
+
except (UnicodeError, ValueError):
|
|
311
|
+
return self._failure_observation(
|
|
312
|
+
input,
|
|
313
|
+
handle,
|
|
314
|
+
source,
|
|
315
|
+
ne.HostStage.REQUEST_WRITE,
|
|
316
|
+
ne.RequestHeaderEncodingUnavailable(),
|
|
317
|
+
)
|
|
318
|
+
except (ConnectionError, OSError, http.client.HTTPException):
|
|
319
|
+
return self._failure_observation(
|
|
320
|
+
input,
|
|
321
|
+
handle,
|
|
322
|
+
source,
|
|
323
|
+
ne.HostStage.REQUEST_WRITE,
|
|
324
|
+
ne.RequestWriteFailed(),
|
|
325
|
+
)
|
|
326
|
+
try:
|
|
327
|
+
response = connection.getresponse()
|
|
328
|
+
except socket.timeout:
|
|
329
|
+
return self._failure_observation(
|
|
330
|
+
input,
|
|
331
|
+
handle,
|
|
332
|
+
source,
|
|
333
|
+
ne.HostStage.RESPONSE_HEADERS,
|
|
334
|
+
ne.Timeout(
|
|
335
|
+
ne.HostStage.RESPONSE_HEADERS,
|
|
336
|
+
ne.HostTimeoutCause.LIBRARY_REPORTED,
|
|
337
|
+
),
|
|
338
|
+
)
|
|
339
|
+
except http.client.RemoteDisconnected:
|
|
340
|
+
return self._failure_observation(
|
|
341
|
+
input,
|
|
342
|
+
handle,
|
|
343
|
+
source,
|
|
344
|
+
ne.HostStage.RESPONSE_HEADERS,
|
|
345
|
+
ne.ResponseHeadersFailed(),
|
|
346
|
+
)
|
|
347
|
+
except http.client.BadStatusLine:
|
|
348
|
+
return self._failure_observation(
|
|
349
|
+
input,
|
|
350
|
+
handle,
|
|
351
|
+
source,
|
|
352
|
+
ne.HostStage.RESPONSE_HEADERS,
|
|
353
|
+
ne.MalformedResponse(),
|
|
354
|
+
)
|
|
355
|
+
except (ConnectionError, OSError, http.client.HTTPException):
|
|
356
|
+
return self._failure_observation(
|
|
357
|
+
input,
|
|
358
|
+
handle,
|
|
359
|
+
source,
|
|
360
|
+
ne.HostStage.RESPONSE_HEADERS,
|
|
361
|
+
ne.ResponseHeadersFailed(),
|
|
362
|
+
)
|
|
363
|
+
try:
|
|
364
|
+
response_headers = tuple(
|
|
365
|
+
self._encode_response_header(name, value)
|
|
366
|
+
for name, value in response.getheaders()
|
|
367
|
+
)
|
|
368
|
+
except (UnicodeError, ValueError, AttributeError):
|
|
369
|
+
return self._failure_observation(
|
|
370
|
+
input,
|
|
371
|
+
handle,
|
|
372
|
+
source,
|
|
373
|
+
ne.HostStage.RESPONSE_HEADERS,
|
|
374
|
+
ne.HeaderRepresentationUnavailable(),
|
|
375
|
+
)
|
|
376
|
+
body = bytearray()
|
|
377
|
+
try:
|
|
378
|
+
while True:
|
|
379
|
+
chunk = response.read(65536)
|
|
380
|
+
if not chunk:
|
|
381
|
+
break
|
|
382
|
+
body.extend(chunk)
|
|
383
|
+
if (
|
|
384
|
+
len(body)
|
|
385
|
+
> input.configuration.response_body_limit
|
|
386
|
+
):
|
|
387
|
+
return self._failure_observation(
|
|
388
|
+
input,
|
|
389
|
+
handle,
|
|
390
|
+
source,
|
|
391
|
+
ne.HostStage.RESPONSE_BODY,
|
|
392
|
+
ne.ResponseBodyLimitExceeded(),
|
|
393
|
+
)
|
|
394
|
+
except socket.timeout:
|
|
395
|
+
return self._failure_observation(
|
|
396
|
+
input,
|
|
397
|
+
handle,
|
|
398
|
+
source,
|
|
399
|
+
ne.HostStage.RESPONSE_BODY,
|
|
400
|
+
ne.Timeout(
|
|
401
|
+
ne.HostStage.RESPONSE_BODY,
|
|
402
|
+
ne.HostTimeoutCause.LIBRARY_REPORTED,
|
|
403
|
+
),
|
|
404
|
+
)
|
|
405
|
+
except (
|
|
406
|
+
http.client.IncompleteRead,
|
|
407
|
+
ConnectionError,
|
|
408
|
+
OSError,
|
|
409
|
+
http.client.HTTPException,
|
|
410
|
+
):
|
|
411
|
+
return self._failure_observation(
|
|
412
|
+
input,
|
|
413
|
+
handle,
|
|
414
|
+
source,
|
|
415
|
+
ne.HostStage.RESPONSE_BODY,
|
|
416
|
+
ne.ResponseBodyFailed(),
|
|
417
|
+
)
|
|
418
|
+
if response.length not in (None, 0):
|
|
419
|
+
return self._failure_observation(
|
|
420
|
+
input,
|
|
421
|
+
handle,
|
|
422
|
+
source,
|
|
423
|
+
ne.HostStage.RESPONSE_BODY,
|
|
424
|
+
ne.ResponseBodyFailed(),
|
|
425
|
+
)
|
|
426
|
+
return ne.ResponseObservation(
|
|
427
|
+
input.attempt,
|
|
428
|
+
handle,
|
|
429
|
+
source,
|
|
430
|
+
ne.RawHttpResponse(
|
|
431
|
+
response.status,
|
|
432
|
+
response_headers,
|
|
433
|
+
tuple(body),
|
|
434
|
+
),
|
|
435
|
+
ne.HeaderProvenance.LIBRARY_OBSERVED,
|
|
436
|
+
ne.Unknown(ne.UnknownEffectReason.NO_RESPONSE),
|
|
437
|
+
)
|
|
438
|
+
finally:
|
|
439
|
+
connection.close()
|
|
440
|
+
|
|
441
|
+
def _new_connection(
|
|
442
|
+
self,
|
|
443
|
+
target: SplitResult,
|
|
444
|
+
) -> http.client.HTTPConnection:
|
|
445
|
+
connection_type = (
|
|
446
|
+
http.client.HTTPSConnection
|
|
447
|
+
if target.scheme == "https"
|
|
448
|
+
else http.client.HTTPConnection
|
|
449
|
+
)
|
|
450
|
+
return connection_type(cast(str, target.hostname), target.port)
|
|
451
|
+
|
|
452
|
+
def _encode_request_header(
|
|
453
|
+
self,
|
|
454
|
+
header: ne.RawHeaderField,
|
|
455
|
+
) -> tuple[str, str]:
|
|
456
|
+
if any(
|
|
457
|
+
value != 9 and (value < 32 or value == 127)
|
|
458
|
+
for value in header.value
|
|
459
|
+
):
|
|
460
|
+
raise ValueError("unrepresentable request header")
|
|
461
|
+
return header.name, bytes(header.value).decode("latin-1")
|
|
462
|
+
|
|
463
|
+
def _encode_response_header(
|
|
464
|
+
self,
|
|
465
|
+
name: str,
|
|
466
|
+
value: str,
|
|
467
|
+
) -> ne.RawHeaderField:
|
|
468
|
+
return ne.RawHeaderField(name, tuple(value.encode("latin-1")))
|
|
469
|
+
|
|
470
|
+
def _enqueue_deadline(
|
|
471
|
+
self,
|
|
472
|
+
handle: ne.ExecutionHandle,
|
|
473
|
+
worker: _Worker,
|
|
474
|
+
) -> None:
|
|
475
|
+
if worker.deadline_emitted:
|
|
476
|
+
return
|
|
477
|
+
source = self._reserve_source(worker)
|
|
478
|
+
observation = self._deadline_observation(
|
|
479
|
+
worker.command.prepared.input,
|
|
480
|
+
handle,
|
|
481
|
+
source,
|
|
482
|
+
)
|
|
483
|
+
if observation is not None:
|
|
484
|
+
worker.deadline_emitted = True
|
|
485
|
+
worker.queue.put(observation)
|
|
486
|
+
|
|
487
|
+
def _deadline_observation(
|
|
488
|
+
self,
|
|
489
|
+
input: ne.AttemptInput,
|
|
490
|
+
handle: ne.ExecutionHandle,
|
|
491
|
+
source: int,
|
|
492
|
+
) -> ne.AdapterObservation | None:
|
|
493
|
+
deadline = input.deadline
|
|
494
|
+
if deadline is None or self._clock is None:
|
|
495
|
+
return None
|
|
496
|
+
observed = self._clock.observe()
|
|
497
|
+
if observed.clock_basis != deadline.clock_basis:
|
|
498
|
+
return self._failure_observation(
|
|
499
|
+
input,
|
|
500
|
+
handle,
|
|
501
|
+
source,
|
|
502
|
+
ne.HostStage.DEADLINE,
|
|
503
|
+
ne.DeadlineControlUnavailable(),
|
|
504
|
+
ne.UnknownEffectReason.DEADLINE,
|
|
505
|
+
)
|
|
506
|
+
if deadline.limit <= observed.value:
|
|
507
|
+
return ne.DeadlineObservation(
|
|
508
|
+
input.attempt,
|
|
509
|
+
handle,
|
|
510
|
+
source,
|
|
511
|
+
deadline,
|
|
512
|
+
observed,
|
|
513
|
+
ne.Unknown(ne.UnknownEffectReason.DEADLINE),
|
|
514
|
+
)
|
|
515
|
+
return None
|
|
516
|
+
|
|
517
|
+
def _worker(
|
|
518
|
+
self,
|
|
519
|
+
handle: ne.ExecutionHandle,
|
|
520
|
+
attempt: ne.AttemptIdentity,
|
|
521
|
+
) -> _Worker | None:
|
|
522
|
+
with self._lock:
|
|
523
|
+
worker = self._workers.get(handle)
|
|
524
|
+
if (
|
|
525
|
+
worker is None
|
|
526
|
+
or worker.command.prepared.input.attempt != attempt
|
|
527
|
+
):
|
|
528
|
+
return None
|
|
529
|
+
return worker
|
|
530
|
+
|
|
531
|
+
def _take(
|
|
532
|
+
self,
|
|
533
|
+
worker: _Worker,
|
|
534
|
+
) -> ne.AdapterObservation | None:
|
|
535
|
+
try:
|
|
536
|
+
return worker.queue.get_nowait()
|
|
537
|
+
except queue.Empty:
|
|
538
|
+
return None
|
|
539
|
+
|
|
540
|
+
def _delivered(
|
|
541
|
+
self,
|
|
542
|
+
worker: _Worker,
|
|
543
|
+
observation: ne.AdapterObservation,
|
|
544
|
+
) -> ne.DeliveredObservation:
|
|
545
|
+
with self._lock:
|
|
546
|
+
delivery = worker.delivery
|
|
547
|
+
worker.delivery += 1
|
|
548
|
+
return ne.DeliveredObservation(delivery, observation)
|
|
549
|
+
|
|
550
|
+
def _reserve_source(self, worker: _Worker) -> int:
|
|
551
|
+
with self._lock:
|
|
552
|
+
source = worker.next_source
|
|
553
|
+
worker.next_source += 1
|
|
554
|
+
return source
|
|
555
|
+
|
|
556
|
+
def _start_failed(
|
|
557
|
+
self,
|
|
558
|
+
input: ne.AttemptInput,
|
|
559
|
+
cause: ne.HostFailure,
|
|
560
|
+
) -> ne.StartFailed:
|
|
561
|
+
return ne.StartFailed(
|
|
562
|
+
self._failure(
|
|
563
|
+
input,
|
|
564
|
+
ne.HostStage.START,
|
|
565
|
+
cause,
|
|
566
|
+
),
|
|
567
|
+
ne.Unknown(ne.UnknownEffectReason.HOST_FAILURE),
|
|
568
|
+
)
|
|
569
|
+
|
|
570
|
+
def _failure_observation(
|
|
571
|
+
self,
|
|
572
|
+
input: ne.AttemptInput,
|
|
573
|
+
handle: ne.ExecutionHandle,
|
|
574
|
+
source: int | None,
|
|
575
|
+
stage: ne.HostStage,
|
|
576
|
+
cause: ne.HostFailure,
|
|
577
|
+
reason: ne.UnknownEffectReason = ne.UnknownEffectReason.HOST_FAILURE,
|
|
578
|
+
) -> ne.FailureObservation:
|
|
579
|
+
return ne.FailureObservation(
|
|
580
|
+
input.attempt,
|
|
581
|
+
handle,
|
|
582
|
+
source,
|
|
583
|
+
self._failure(input, stage, cause),
|
|
584
|
+
ne.Unknown(reason),
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
@staticmethod
|
|
588
|
+
def _failure(
|
|
589
|
+
input: ne.AttemptInput,
|
|
590
|
+
stage: ne.HostStage,
|
|
591
|
+
cause: ne.HostFailure,
|
|
592
|
+
) -> ne.NetworkHostFailure:
|
|
593
|
+
return ne.NetworkHostFailure(
|
|
594
|
+
input.attempt,
|
|
595
|
+
stage,
|
|
596
|
+
input.configuration,
|
|
597
|
+
cause,
|
|
598
|
+
)
|
|
599
|
+
|
|
600
|
+
@staticmethod
|
|
601
|
+
def _classify_exception(
|
|
602
|
+
error: BaseException,
|
|
603
|
+
) -> tuple[ne.HostStage, ne.HostFailure]:
|
|
604
|
+
if isinstance(error, socket.gaierror):
|
|
605
|
+
return ne.HostStage.REQUEST_WRITE, ne.NameResolutionFailed()
|
|
606
|
+
if isinstance(error, socket.timeout):
|
|
607
|
+
return (
|
|
608
|
+
ne.HostStage.RESPONSE_BODY,
|
|
609
|
+
ne.Timeout(
|
|
610
|
+
ne.HostStage.RESPONSE_BODY,
|
|
611
|
+
ne.HostTimeoutCause.LIBRARY_REPORTED,
|
|
612
|
+
),
|
|
613
|
+
)
|
|
614
|
+
return ne.HostStage.RESPONSE_BODY, ne.HostRuntimeFailure()
|
|
615
|
+
|
|
616
|
+
|
|
617
|
+
__all__ = [
|
|
618
|
+
"CancellationPort",
|
|
619
|
+
"EventCancellationPort",
|
|
620
|
+
"HttpHostAdapter",
|
|
621
|
+
"MonotonicClockPort",
|
|
622
|
+
"SystemMonotonicClock",
|
|
623
|
+
]
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: necoherence-http-adapter
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: HTTP host adapter for Necoherence
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
License-File: LICENSE
|
|
7
|
+
Requires-Python: >=3.12
|
|
8
|
+
Requires-Dist: necoherence==0.2.0
|
|
9
|
+
Provides-Extra: dev
|
|
10
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
11
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
necoherence_http_adapter/__init__.py,sha256=kXAq9s1GMveh7qNW2w3e3Nyi38wVOSKKDpLlLQN_OE8,20466
|
|
2
|
+
necoherence_http_adapter-0.2.0.dist-info/METADATA,sha256=0nFzyH9wse6B68tc7ukca6OAXZ8QN-MUBCrWXQBPB8s,309
|
|
3
|
+
necoherence_http_adapter-0.2.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
|
4
|
+
necoherence_http_adapter-0.2.0.dist-info/licenses/LICENSE,sha256=JbpxLbnfFXzSWAmI-qa8ddABbmNbok6264R7DhXRAFM,1065
|
|
5
|
+
necoherence_http_adapter-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 barineco
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|