telemetry-dev 0.2.13__tar.gz → 0.3.1__tar.gz
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.
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/PKG-INFO +13 -8
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/README.md +12 -7
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/pyproject.toml +1 -1
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/pyproject.toml.orig +1 -1
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_config.py +1 -1
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_prompts.py +151 -29
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/LICENSE +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/__init__.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_capture.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_client.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_context.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_export.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_logs.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_metrics.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_observe.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_processor.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_semconv.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_serialize.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/_spans.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/otel.py +0 -0
- {telemetry_dev-0.2.13 → telemetry_dev-0.3.1}/src/telemetry_dev/py.typed +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: telemetry-dev
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: telemetry.dev SDK for Python — OpenTelemetry-native GenAI tracing, logs, and metrics
|
|
5
5
|
Keywords: telemetry,opentelemetry,llm,genai,tracing,observability
|
|
6
6
|
Author: telemetry.dev
|
|
@@ -81,7 +81,7 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
|
|
|
81
81
|
| Variable | Default | Purpose |
|
|
82
82
|
| --- | --- | --- |
|
|
83
83
|
| `TELEMETRY_DEV_API_KEY` | — | Ingest key (`td_live_...`). Absent = SDK is a no-op. |
|
|
84
|
-
| `TELEMETRY_DEV_BASE_URL` | `https://
|
|
84
|
+
| `TELEMETRY_DEV_BASE_URL` | `https://api.telemetry.dev` | Base URL for telemetry export and `get_prompt` (trailing slashes stripped). |
|
|
85
85
|
| `TELEMETRY_DEV_ENVIRONMENT` | `production` | Deployment environment label. |
|
|
86
86
|
| `OTEL_SERVICE_NAME` | `unknown_service` | Service name on every trace. |
|
|
87
87
|
| `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | OTel trace sampling policy, which includes session roots. |
|
|
@@ -89,6 +89,10 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
|
|
|
89
89
|
|
|
90
90
|
Explicit `init()` arguments take precedence over environment variables.
|
|
91
91
|
|
|
92
|
+
Earlier versions sent data to `https://ingest.telemetry.dev` by default. That hostname reaches the
|
|
93
|
+
same service and stays supported: if a firewall allows only it, set `TELEMETRY_DEV_BASE_URL` or
|
|
94
|
+
`base_url` to it.
|
|
95
|
+
|
|
92
96
|
Root spans remain independent by default. `telemetry_dev.init(session_mode="process")` groups
|
|
93
97
|
otherwise uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK
|
|
94
98
|
initialization. Explicit and propagated session IDs take precedence, and valid parent trace
|
|
@@ -253,7 +257,7 @@ pinned = telemetry_dev.get_prompt("support/reply", version=13)
|
|
|
253
257
|
without its prompt.
|
|
254
258
|
- `get_prompt` uses the API key and base URL of an enabled `init()`, or `TELEMETRY_DEV_API_KEY`
|
|
255
259
|
and `TELEMETRY_DEV_BASE_URL`, or its own `api_key` and `base_url`. It reads the same host that
|
|
256
|
-
telemetry goes to, `https://
|
|
260
|
+
telemetry goes to, `https://api.telemetry.dev` by default. The key needs the read scope for
|
|
257
261
|
prompts.
|
|
258
262
|
- When a label sends part of the traffic to a second version, each session keeps one version: the
|
|
259
263
|
choice uses the session id of `propagate_attributes` (or of `session_mode="process"`). Without a
|
|
@@ -272,8 +276,9 @@ ran in it, or before it started in the code that started it; with none, it recor
|
|
|
272
276
|
for code that has not compiled in its own task. Model calls are the spans of type `generation`
|
|
273
277
|
(operations `chat`, `text_completion`, and `generate_content`), including the spans of the provider
|
|
274
278
|
integrations: OpenAI, Anthropic, Amazon Bedrock, Google GenAI, LiteLLM, and OpenRouter. Spans from
|
|
275
|
-
other OpenTelemetry tracers do not get the attributes.
|
|
276
|
-
|
|
279
|
+
other OpenTelemetry tracers do not get the attributes. A model call outside `with_prompt` does not
|
|
280
|
+
get the attributes, even when it sends messages from `compile()`; with input capture on, the call
|
|
281
|
+
still records those messages as its input.
|
|
277
282
|
|
|
278
283
|
## Auto-metrics
|
|
279
284
|
|
|
@@ -331,7 +336,7 @@ from telemetry_dev import TelemetrySpanProcessor
|
|
|
331
336
|
| Option | Default | Purpose |
|
|
332
337
|
| --- | --- | --- |
|
|
333
338
|
| `api_key` | `TELEMETRY_DEV_API_KEY` | Ingest key. Absent with no `span_exporter` = inert no-op. |
|
|
334
|
-
| `base_url` | `TELEMETRY_DEV_BASE_URL` or `https://
|
|
339
|
+
| `base_url` | `TELEMETRY_DEV_BASE_URL` or `https://api.telemetry.dev` | Base URL; trailing slashes are stripped. |
|
|
335
340
|
| `export_mode` | `"batched"` | `"batched"` or `"immediate"` span export. |
|
|
336
341
|
| `max_export_batch_size` | `64` | BatchSpanProcessor export batch size. |
|
|
337
342
|
| `schedule_delay_millis` | `1000` | BatchSpanProcessor schedule delay. |
|
|
@@ -399,7 +404,7 @@ from opentelemetry.sdk.trace import TracerProvider
|
|
|
399
404
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
400
405
|
|
|
401
406
|
exporter = OTLPSpanExporter(
|
|
402
|
-
endpoint="https://
|
|
407
|
+
endpoint="https://api.telemetry.dev/v1/traces",
|
|
403
408
|
headers={"Authorization": f"Bearer {os.environ['TELEMETRY_DEV_API_KEY']}"},
|
|
404
409
|
)
|
|
405
410
|
provider = TracerProvider()
|
|
@@ -407,7 +412,7 @@ provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
|
407
412
|
trace.set_tracer_provider(provider)
|
|
408
413
|
```
|
|
409
414
|
|
|
410
|
-
The Claude API can also send server-side spans to telemetry.dev through a Console destination. Set the endpoint to `https://
|
|
415
|
+
The Claude API can also send server-side spans to telemetry.dev through a Console destination. Set the endpoint to `https://api.telemetry.dev`. Set the `Authorization` header to `Bearer <telemetry.dev API key>`. For more information, refer to [Claude Platform](https://telemetry.dev/docs/integrations/claude-platform).
|
|
411
416
|
|
|
412
417
|
When the SDK and the Claude API send the spans of a request to the same telemetry.dev project, telemetry.dev counts the tokens of the request one time, from the API span. The SDK span stays in the trace with no tokens. telemetry.dev matches the two spans by the request ID or the response ID, so the API span does not have to be a child of the SDK span. If the Console destination uses an API key that is scoped to one environment, its API spans take over only the SDK spans of that environment.
|
|
413
418
|
|
|
@@ -56,7 +56,7 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
|
|
|
56
56
|
| Variable | Default | Purpose |
|
|
57
57
|
| --- | --- | --- |
|
|
58
58
|
| `TELEMETRY_DEV_API_KEY` | — | Ingest key (`td_live_...`). Absent = SDK is a no-op. |
|
|
59
|
-
| `TELEMETRY_DEV_BASE_URL` | `https://
|
|
59
|
+
| `TELEMETRY_DEV_BASE_URL` | `https://api.telemetry.dev` | Base URL for telemetry export and `get_prompt` (trailing slashes stripped). |
|
|
60
60
|
| `TELEMETRY_DEV_ENVIRONMENT` | `production` | Deployment environment label. |
|
|
61
61
|
| `OTEL_SERVICE_NAME` | `unknown_service` | Service name on every trace. |
|
|
62
62
|
| `OTEL_TRACES_SAMPLER` | `parentbased_always_on` | OTel trace sampling policy, which includes session roots. |
|
|
@@ -64,6 +64,10 @@ routed to the `on_error` hook / `telemetry_dev` logger — never raised into you
|
|
|
64
64
|
|
|
65
65
|
Explicit `init()` arguments take precedence over environment variables.
|
|
66
66
|
|
|
67
|
+
Earlier versions sent data to `https://ingest.telemetry.dev` by default. That hostname reaches the
|
|
68
|
+
same service and stays supported: if a firewall allows only it, set `TELEMETRY_DEV_BASE_URL` or
|
|
69
|
+
`base_url` to it.
|
|
70
|
+
|
|
67
71
|
Root spans remain independent by default. `telemetry_dev.init(session_mode="process")` groups
|
|
68
72
|
otherwise uncorrelated spans and logs using a fresh opaque UUID for each enabled SDK
|
|
69
73
|
initialization. Explicit and propagated session IDs take precedence, and valid parent trace
|
|
@@ -228,7 +232,7 @@ pinned = telemetry_dev.get_prompt("support/reply", version=13)
|
|
|
228
232
|
without its prompt.
|
|
229
233
|
- `get_prompt` uses the API key and base URL of an enabled `init()`, or `TELEMETRY_DEV_API_KEY`
|
|
230
234
|
and `TELEMETRY_DEV_BASE_URL`, or its own `api_key` and `base_url`. It reads the same host that
|
|
231
|
-
telemetry goes to, `https://
|
|
235
|
+
telemetry goes to, `https://api.telemetry.dev` by default. The key needs the read scope for
|
|
232
236
|
prompts.
|
|
233
237
|
- When a label sends part of the traffic to a second version, each session keeps one version: the
|
|
234
238
|
choice uses the session id of `propagate_attributes` (or of `session_mode="process"`). Without a
|
|
@@ -247,8 +251,9 @@ ran in it, or before it started in the code that started it; with none, it recor
|
|
|
247
251
|
for code that has not compiled in its own task. Model calls are the spans of type `generation`
|
|
248
252
|
(operations `chat`, `text_completion`, and `generate_content`), including the spans of the provider
|
|
249
253
|
integrations: OpenAI, Anthropic, Amazon Bedrock, Google GenAI, LiteLLM, and OpenRouter. Spans from
|
|
250
|
-
other OpenTelemetry tracers do not get the attributes.
|
|
251
|
-
|
|
254
|
+
other OpenTelemetry tracers do not get the attributes. A model call outside `with_prompt` does not
|
|
255
|
+
get the attributes, even when it sends messages from `compile()`; with input capture on, the call
|
|
256
|
+
still records those messages as its input.
|
|
252
257
|
|
|
253
258
|
## Auto-metrics
|
|
254
259
|
|
|
@@ -306,7 +311,7 @@ from telemetry_dev import TelemetrySpanProcessor
|
|
|
306
311
|
| Option | Default | Purpose |
|
|
307
312
|
| --- | --- | --- |
|
|
308
313
|
| `api_key` | `TELEMETRY_DEV_API_KEY` | Ingest key. Absent with no `span_exporter` = inert no-op. |
|
|
309
|
-
| `base_url` | `TELEMETRY_DEV_BASE_URL` or `https://
|
|
314
|
+
| `base_url` | `TELEMETRY_DEV_BASE_URL` or `https://api.telemetry.dev` | Base URL; trailing slashes are stripped. |
|
|
310
315
|
| `export_mode` | `"batched"` | `"batched"` or `"immediate"` span export. |
|
|
311
316
|
| `max_export_batch_size` | `64` | BatchSpanProcessor export batch size. |
|
|
312
317
|
| `schedule_delay_millis` | `1000` | BatchSpanProcessor schedule delay. |
|
|
@@ -374,7 +379,7 @@ from opentelemetry.sdk.trace import TracerProvider
|
|
|
374
379
|
from opentelemetry.sdk.trace.export import BatchSpanProcessor
|
|
375
380
|
|
|
376
381
|
exporter = OTLPSpanExporter(
|
|
377
|
-
endpoint="https://
|
|
382
|
+
endpoint="https://api.telemetry.dev/v1/traces",
|
|
378
383
|
headers={"Authorization": f"Bearer {os.environ['TELEMETRY_DEV_API_KEY']}"},
|
|
379
384
|
)
|
|
380
385
|
provider = TracerProvider()
|
|
@@ -382,7 +387,7 @@ provider.add_span_processor(BatchSpanProcessor(exporter))
|
|
|
382
387
|
trace.set_tracer_provider(provider)
|
|
383
388
|
```
|
|
384
389
|
|
|
385
|
-
The Claude API can also send server-side spans to telemetry.dev through a Console destination. Set the endpoint to `https://
|
|
390
|
+
The Claude API can also send server-side spans to telemetry.dev through a Console destination. Set the endpoint to `https://api.telemetry.dev`. Set the `Authorization` header to `Bearer <telemetry.dev API key>`. For more information, refer to [Claude Platform](https://telemetry.dev/docs/integrations/claude-platform).
|
|
386
391
|
|
|
387
392
|
When the SDK and the Claude API send the spans of a request to the same telemetry.dev project, telemetry.dev counts the tokens of the request one time, from the API span. The SDK span stays in the trace with no tokens. telemetry.dev matches the two spans by the request ID or the response ID, so the API span does not have to be a child of the SDK span. If the Console destination uses an API key that is scoped to one environment, its API spans take over only the SDK spans of that environment.
|
|
388
393
|
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
|
+
import contextlib
|
|
4
5
|
import copy
|
|
5
6
|
import hashlib
|
|
6
7
|
import json
|
|
7
8
|
import random
|
|
8
9
|
import re
|
|
10
|
+
import socket
|
|
9
11
|
import threading
|
|
10
12
|
import urllib.error
|
|
11
13
|
import urllib.parse
|
|
@@ -26,7 +28,7 @@ from opentelemetry import context as otel_context
|
|
|
26
28
|
from opentelemetry.context import Context
|
|
27
29
|
|
|
28
30
|
from ._client import Client, get_client
|
|
29
|
-
from ._config import report_error, resolve_config
|
|
31
|
+
from ._config import SDK_VERSION, report_error, resolve_config
|
|
30
32
|
from ._context import propagated_attributes
|
|
31
33
|
from ._semconv import (
|
|
32
34
|
ATTR_PROMPT_LABEL,
|
|
@@ -274,7 +276,8 @@ def prompt_attributes(
|
|
|
274
276
|
client: Client, context: Context | None, operation: object, capture_input: bool
|
|
275
277
|
) -> dict[str, AttributeValue]:
|
|
276
278
|
"""The td.prompt.* attributes for a span with this operation that starts in ``context``."""
|
|
277
|
-
|
|
279
|
+
# An attribute can be a list, which a frozenset cannot hold.
|
|
280
|
+
if not isinstance(operation, str) or operation not in _MODEL_CALL_OPERATIONS:
|
|
278
281
|
return {}
|
|
279
282
|
scope = otel_context.get_value(_PROMPT_KEY, context) or otel_context.get_value(_PROMPT_KEY)
|
|
280
283
|
if not isinstance(scope, _PromptScope):
|
|
@@ -333,7 +336,13 @@ def _get_prompt(
|
|
|
333
336
|
cache_ttl_seconds,
|
|
334
337
|
lambda: _fetch(
|
|
335
338
|
f"{url}/v1/prompts?{urllib.parse.urlencode(query)}",
|
|
336
|
-
{
|
|
339
|
+
{
|
|
340
|
+
"Authorization": f"Bearer {key}",
|
|
341
|
+
"X-Telemetry-Dev-SDK": "telemetry-dev",
|
|
342
|
+
# urllib's default User-Agent, Python-urllib/3.x, is refused by bot filters
|
|
343
|
+
# such as Cloudflare's Browser Integrity Check.
|
|
344
|
+
"User-Agent": f"telemetry-dev-python/{SDK_VERSION}",
|
|
345
|
+
},
|
|
337
346
|
),
|
|
338
347
|
on_error,
|
|
339
348
|
)
|
|
@@ -461,10 +470,9 @@ def _load(
|
|
|
461
470
|
on_error: Callable[[BaseException], None] | None,
|
|
462
471
|
) -> dict[str, Any]:
|
|
463
472
|
"""Return the kept copy of a prompt, and fetch a new copy in a background thread when it is
|
|
464
|
-
older than the TTL. Only a call with no copy waits for a fetch, until the fetch's deadline
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
fetch."""
|
|
473
|
+
older than the TTL. Only a call with no copy waits for a fetch, until the fetch's deadline. A
|
|
474
|
+
failed fetch with no copy fails the calls of the next ``_ERROR_TTL_S`` (or the TTL, if it is
|
|
475
|
+
shorter) without a fetch."""
|
|
468
476
|
with _cache_lock:
|
|
469
477
|
entry = _cache.get(key)
|
|
470
478
|
if entry is None:
|
|
@@ -483,7 +491,7 @@ def _load(
|
|
|
483
491
|
return entry.response
|
|
484
492
|
pending, deadline = entry.pending, entry.deadline
|
|
485
493
|
if pending is not None and not pending.wait(deadline - monotonic()):
|
|
486
|
-
raise
|
|
494
|
+
raise _timeout()
|
|
487
495
|
with _cache_lock:
|
|
488
496
|
response, error = entry.response, entry.error
|
|
489
497
|
if response is not None:
|
|
@@ -542,31 +550,141 @@ class _NoRedirect(urllib.request.HTTPRedirectHandler):
|
|
|
542
550
|
return None
|
|
543
551
|
|
|
544
552
|
|
|
545
|
-
|
|
553
|
+
class _Deadline:
|
|
554
|
+
"""Ends one prompt fetch after ``_FETCH_TIMEOUT_S``. The timeout of urllib applies to each
|
|
555
|
+
socket operation, so a server that sends a byte now and then could keep a fetch reading forever,
|
|
556
|
+
and each address of a host gets the full timeout to connect. A connection attempt gets only the
|
|
557
|
+
time that is left, and at the deadline this shuts the connection down, which ends a blocked
|
|
558
|
+
read. It cannot stop an address lookup; the system resolver bounds that."""
|
|
559
|
+
|
|
560
|
+
def __init__(self) -> None:
|
|
561
|
+
self.expired = False
|
|
562
|
+
self._end = monotonic() + _FETCH_TIMEOUT_S
|
|
563
|
+
self._lock = threading.Lock()
|
|
564
|
+
self._sockets: list[socket.socket] = []
|
|
565
|
+
self._timer = threading.Timer(_FETCH_TIMEOUT_S, self._expire)
|
|
566
|
+
self._timer.daemon = True
|
|
567
|
+
self._timer.start()
|
|
568
|
+
|
|
569
|
+
def connect(
|
|
570
|
+
self,
|
|
571
|
+
address: tuple[str, int],
|
|
572
|
+
timeout: float | None,
|
|
573
|
+
source_address: tuple[str, int] | None = None,
|
|
574
|
+
) -> socket.socket:
|
|
575
|
+
"""``socket.create_connection``, with each attempt bounded by the deadline."""
|
|
576
|
+
host, port = address
|
|
577
|
+
error: OSError = OSError(f"no address for {host}")
|
|
578
|
+
for family, kind, proto, _, target in socket.getaddrinfo(host, port, 0, socket.SOCK_STREAM):
|
|
579
|
+
left = self._end - monotonic()
|
|
580
|
+
if left <= 0:
|
|
581
|
+
break
|
|
582
|
+
sock = socket.socket(family, kind, proto)
|
|
583
|
+
try:
|
|
584
|
+
sock.settimeout(left if timeout is None else min(left, timeout))
|
|
585
|
+
if source_address is not None:
|
|
586
|
+
sock.bind(source_address)
|
|
587
|
+
sock.connect(target)
|
|
588
|
+
sock.settimeout(timeout)
|
|
589
|
+
except OSError as exc:
|
|
590
|
+
sock.close()
|
|
591
|
+
error = exc
|
|
592
|
+
continue
|
|
593
|
+
with self._lock:
|
|
594
|
+
if not self.expired:
|
|
595
|
+
try:
|
|
596
|
+
# A TLS wrap takes the file descriptor of sock, so keep a copy to shut down.
|
|
597
|
+
self._sockets.append(sock.dup())
|
|
598
|
+
except OSError:
|
|
599
|
+
sock.close()
|
|
600
|
+
raise
|
|
601
|
+
return sock
|
|
602
|
+
sock.close()
|
|
603
|
+
break
|
|
604
|
+
if self.expired or self._end <= monotonic():
|
|
605
|
+
raise TimeoutError("the prompt fetch passed its deadline")
|
|
606
|
+
raise error
|
|
607
|
+
|
|
608
|
+
def _expire(self) -> None:
|
|
609
|
+
with self._lock:
|
|
610
|
+
self.expired = True
|
|
611
|
+
for sock in self._sockets:
|
|
612
|
+
with contextlib.suppress(OSError):
|
|
613
|
+
sock.shutdown(socket.SHUT_RDWR)
|
|
614
|
+
|
|
615
|
+
def close(self) -> None:
|
|
616
|
+
self._timer.cancel()
|
|
617
|
+
with self._lock:
|
|
618
|
+
for sock in self._sockets:
|
|
619
|
+
sock.close()
|
|
620
|
+
self._sockets.clear()
|
|
621
|
+
|
|
622
|
+
|
|
623
|
+
class _Request(urllib.request.Request):
|
|
624
|
+
def __init__(self, url: str, headers: dict[str, str], deadline: _Deadline) -> None:
|
|
625
|
+
super().__init__(url, headers=headers, method="GET")
|
|
626
|
+
self.deadline = deadline
|
|
627
|
+
|
|
628
|
+
|
|
629
|
+
def _on_deadline(http_class: Any, request: urllib.request.Request) -> Any:
|
|
630
|
+
"""``http_class``, with the sockets of a ``_Request`` under its deadline."""
|
|
631
|
+
|
|
632
|
+
def connection(host: str, **kwargs: Any) -> Any:
|
|
633
|
+
conn = http_class(host, **kwargs)
|
|
634
|
+
if isinstance(request, _Request):
|
|
635
|
+
conn._create_connection = request.deadline.connect
|
|
636
|
+
return conn
|
|
637
|
+
|
|
638
|
+
return connection
|
|
639
|
+
|
|
640
|
+
|
|
641
|
+
class _HTTPHandler(urllib.request.HTTPHandler):
|
|
642
|
+
def do_open(self, http_class: Any, req: urllib.request.Request, **http_conn_args: Any) -> Any:
|
|
643
|
+
return super().do_open(_on_deadline(http_class, req), req, **http_conn_args)
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
class _HTTPSHandler(urllib.request.HTTPSHandler):
|
|
647
|
+
def do_open(self, http_class: Any, req: urllib.request.Request, **http_conn_args: Any) -> Any:
|
|
648
|
+
return super().do_open(_on_deadline(http_class, req), req, **http_conn_args)
|
|
649
|
+
|
|
650
|
+
|
|
651
|
+
_opener = urllib.request.build_opener(_NoRedirect, _HTTPHandler, _HTTPSHandler)
|
|
546
652
|
|
|
547
653
|
|
|
548
654
|
def _fetch(url: str, headers: dict[str, str]) -> dict[str, Any]:
|
|
549
|
-
|
|
655
|
+
deadline = _Deadline()
|
|
656
|
+
retry_after: str | None = None
|
|
550
657
|
try:
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
658
|
+
try:
|
|
659
|
+
with _opener.open(
|
|
660
|
+
_Request(url, headers, deadline), timeout=_FETCH_TIMEOUT_S
|
|
661
|
+
) as response:
|
|
662
|
+
status: int = response.status
|
|
663
|
+
body: bytes = response.read()
|
|
664
|
+
except urllib.error.HTTPError as exc:
|
|
665
|
+
with exc:
|
|
666
|
+
status, body = exc.code, exc.read()
|
|
667
|
+
retry_after = exc.headers.get("Retry-After")
|
|
668
|
+
except Exception as exc:
|
|
669
|
+
if (
|
|
670
|
+
deadline.expired
|
|
671
|
+
or isinstance(exc, TimeoutError)
|
|
672
|
+
or isinstance(getattr(exc, "reason", None), TimeoutError)
|
|
673
|
+
):
|
|
674
|
+
raise _timeout() from exc
|
|
675
|
+
if isinstance(exc, (urllib.error.URLError, OSError)):
|
|
676
|
+
raise PromptError("network_error", "Could not reach telemetry.dev") from exc
|
|
677
|
+
raise
|
|
678
|
+
finally:
|
|
679
|
+
deadline.close()
|
|
680
|
+
# A read that the deadline cut short can end without an error, with part of the body.
|
|
681
|
+
if deadline.expired:
|
|
682
|
+
raise _timeout()
|
|
683
|
+
if not 200 <= status < 300:
|
|
684
|
+
error = _api_error(status, body)
|
|
685
|
+
if status in (429, 503):
|
|
686
|
+
error._retry_after = _retry_after(retry_after) # pyright: ignore[reportPrivateUsage]
|
|
687
|
+
raise error
|
|
570
688
|
try:
|
|
571
689
|
parsed = _parse_response(json.loads(body))
|
|
572
690
|
except ValueError:
|
|
@@ -576,6 +694,10 @@ def _fetch(url: str, headers: dict[str, str]) -> dict[str, Any]:
|
|
|
576
694
|
return parsed
|
|
577
695
|
|
|
578
696
|
|
|
697
|
+
def _timeout() -> PromptError:
|
|
698
|
+
return PromptError("timeout", f"Fetching the prompt took more than {_FETCH_TIMEOUT_S:g} s")
|
|
699
|
+
|
|
700
|
+
|
|
579
701
|
def _api_error(status: int, body: bytes) -> PromptError:
|
|
580
702
|
try:
|
|
581
703
|
error = json.loads(body).get("error")
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|