abto 0.0.1__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.
- abto/__init__.py +35 -0
- abto/client.py +62 -0
- abto/context.py +83 -0
- abto/events.py +1703 -0
- abto-0.0.1.dist-info/METADATA +83 -0
- abto-0.0.1.dist-info/RECORD +7 -0
- abto-0.0.1.dist-info/WHEEL +4 -0
abto/__init__.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""`abto` — ABTO server-side SDK for Python.
|
|
2
|
+
|
|
3
|
+
Thin gateway header helper: carries x-abto-* identifiers via contextvars and
|
|
4
|
+
injects them into outbound provider calls (httpx event hooks). The gateway owns
|
|
5
|
+
token, cost, latency, request_id, and variant assignment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from .client import Abto, abto_request_hook, create_abto
|
|
9
|
+
from .context import (
|
|
10
|
+
ABTO_HEADER,
|
|
11
|
+
AbtoContext,
|
|
12
|
+
create_trace_id,
|
|
13
|
+
create_traceparent,
|
|
14
|
+
get_context,
|
|
15
|
+
get_headers,
|
|
16
|
+
set_context,
|
|
17
|
+
with_context,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
__version__ = "0.0.1"
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Abto",
|
|
24
|
+
"create_abto",
|
|
25
|
+
"abto_request_hook",
|
|
26
|
+
"AbtoContext",
|
|
27
|
+
"ABTO_HEADER",
|
|
28
|
+
"with_context",
|
|
29
|
+
"get_context",
|
|
30
|
+
"set_context",
|
|
31
|
+
"get_headers",
|
|
32
|
+
"create_trace_id",
|
|
33
|
+
"create_traceparent",
|
|
34
|
+
"__version__",
|
|
35
|
+
]
|
abto/client.py
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"""Thin server facade: gateway baseURL + ABTO header injection.
|
|
2
|
+
|
|
3
|
+
It does not compute token/cost/latency. It routes provider SDK calls through the
|
|
4
|
+
ABTO Gateway and carries the x-abto-* identifiers from the current context.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from typing import Any, Callable, Dict, List, Optional
|
|
10
|
+
|
|
11
|
+
from .context import AbtoContext, create_trace_id, get_headers, with_context
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def abto_request_hook() -> Callable[[Any], None]:
|
|
15
|
+
"""httpx request event hook: inject ABTO headers from the current context."""
|
|
16
|
+
|
|
17
|
+
def hook(request: Any) -> None:
|
|
18
|
+
for key, value in get_headers().items():
|
|
19
|
+
request.headers.setdefault(key, value)
|
|
20
|
+
|
|
21
|
+
return hook
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class Abto:
|
|
25
|
+
def __init__(self, api_key: Optional[str] = None, gateway_base_url: Optional[str] = None) -> None:
|
|
26
|
+
self.api_key = api_key
|
|
27
|
+
self.gateway_base_url = gateway_base_url
|
|
28
|
+
|
|
29
|
+
def get_headers(self, ctx: Optional[AbtoContext] = None) -> Dict[str, str]:
|
|
30
|
+
return get_headers(ctx)
|
|
31
|
+
|
|
32
|
+
def with_context(self, **kwargs: Optional[str]):
|
|
33
|
+
return with_context(**kwargs)
|
|
34
|
+
|
|
35
|
+
def create_trace_id(self) -> str:
|
|
36
|
+
return create_trace_id()
|
|
37
|
+
|
|
38
|
+
def httpx_event_hooks(self) -> Dict[str, List[Callable[[Any], None]]]:
|
|
39
|
+
return {"request": [abto_request_hook()]}
|
|
40
|
+
|
|
41
|
+
def openai(self, **client_kwargs: Any) -> Any:
|
|
42
|
+
"""Construct an OpenAI client pointed at the gateway with header injection.
|
|
43
|
+
|
|
44
|
+
Requires the optional `openai` and `httpx` extras.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
import httpx
|
|
48
|
+
from openai import OpenAI
|
|
49
|
+
except ImportError as exc: # pragma: no cover - optional dependency
|
|
50
|
+
raise ImportError("abto.openai() requires the 'openai' extra: pip install 'abto[openai]'") from exc
|
|
51
|
+
|
|
52
|
+
http_client = httpx.Client(event_hooks=self.httpx_event_hooks())
|
|
53
|
+
return OpenAI(
|
|
54
|
+
api_key=self.api_key,
|
|
55
|
+
base_url=self.gateway_base_url,
|
|
56
|
+
http_client=http_client,
|
|
57
|
+
**client_kwargs,
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def create_abto(api_key: Optional[str] = None, gateway_base_url: Optional[str] = None) -> Abto:
|
|
62
|
+
return Abto(api_key=api_key, gateway_base_url=gateway_base_url)
|
abto/context.py
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Request-scoped ABTO identifier context for the `abto` Python package.
|
|
2
|
+
|
|
3
|
+
Mirrors @abto-app/sdk/server: carries the gateway identifier headers via
|
|
4
|
+
contextvars so outbound provider calls can attach them. The gateway remains the
|
|
5
|
+
source of truth for token, cost, latency, request_id, and variant assignment.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import contextvars
|
|
11
|
+
import secrets
|
|
12
|
+
from contextlib import contextmanager
|
|
13
|
+
from dataclasses import dataclass, replace
|
|
14
|
+
from typing import Dict, Iterator, Optional
|
|
15
|
+
|
|
16
|
+
ABTO_HEADER = {
|
|
17
|
+
"user_id": "x-abto-device-id",
|
|
18
|
+
"node_id": "x-abto-node-key",
|
|
19
|
+
"traceparent": "traceparent",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class AbtoContext:
|
|
25
|
+
"""End-user id, "feature.node" node id, and end-user action trace id."""
|
|
26
|
+
|
|
27
|
+
user_id: Optional[str] = None
|
|
28
|
+
node_id: Optional[str] = None
|
|
29
|
+
trace_id: Optional[str] = None
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_current: contextvars.ContextVar[AbtoContext] = contextvars.ContextVar(
|
|
33
|
+
"abto_context", default=AbtoContext()
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def get_context() -> AbtoContext:
|
|
38
|
+
return _current.get()
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def set_context(
|
|
42
|
+
user_id: Optional[str] = None,
|
|
43
|
+
node_id: Optional[str] = None,
|
|
44
|
+
trace_id: Optional[str] = None,
|
|
45
|
+
) -> None:
|
|
46
|
+
patch = {k: v for k, v in dict(user_id=user_id, node_id=node_id, trace_id=trace_id).items() if v is not None}
|
|
47
|
+
_current.set(replace(_current.get(), **patch))
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@contextmanager
|
|
51
|
+
def with_context(
|
|
52
|
+
user_id: Optional[str] = None,
|
|
53
|
+
node_id: Optional[str] = None,
|
|
54
|
+
trace_id: Optional[str] = None,
|
|
55
|
+
) -> Iterator[AbtoContext]:
|
|
56
|
+
patch = {k: v for k, v in dict(user_id=user_id, node_id=node_id, trace_id=trace_id).items() if v is not None}
|
|
57
|
+
merged = replace(_current.get(), **patch)
|
|
58
|
+
token = _current.set(merged)
|
|
59
|
+
try:
|
|
60
|
+
yield merged
|
|
61
|
+
finally:
|
|
62
|
+
_current.reset(token)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def create_trace_id() -> str:
|
|
66
|
+
"""32-hex-char trace id, per W3C trace-context."""
|
|
67
|
+
return secrets.token_hex(16)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def create_traceparent(trace_id: str) -> str:
|
|
71
|
+
return f"00-{trace_id}-{secrets.token_hex(8)}-01"
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_headers(ctx: Optional[AbtoContext] = None) -> Dict[str, str]:
|
|
75
|
+
c = ctx if ctx is not None else _current.get()
|
|
76
|
+
headers: Dict[str, str] = {}
|
|
77
|
+
if c.user_id:
|
|
78
|
+
headers[ABTO_HEADER["user_id"]] = c.user_id
|
|
79
|
+
if c.node_id:
|
|
80
|
+
headers[ABTO_HEADER["node_id"]] = c.node_id
|
|
81
|
+
if c.trace_id:
|
|
82
|
+
headers[ABTO_HEADER["traceparent"]] = create_traceparent(c.trace_id)
|
|
83
|
+
return headers
|