abto 0.0.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.
- abto-0.0.1/.gitignore +46 -0
- abto-0.0.1/PKG-INFO +83 -0
- abto-0.0.1/README.md +67 -0
- abto-0.0.1/abto/__init__.py +35 -0
- abto-0.0.1/abto/client.py +62 -0
- abto-0.0.1/abto/context.py +83 -0
- abto-0.0.1/abto/events.py +1703 -0
- abto-0.0.1/pyproject.toml +23 -0
- abto-0.0.1/tests/test_context.py +36 -0
abto-0.0.1/.gitignore
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Go
|
|
2
|
+
/bin/
|
|
3
|
+
*.exe
|
|
4
|
+
*.test
|
|
5
|
+
*.out
|
|
6
|
+
# ad-hoc `go build ./cmd/<x>` 산출물(정규 빌드는 bin/). 경로 앵커라 cmd/smoke 소스 디렉토리는 건드리지 않는다.
|
|
7
|
+
/apps/gateway/smoke
|
|
8
|
+
/apps/gateway/gateway
|
|
9
|
+
|
|
10
|
+
# macOS
|
|
11
|
+
.DS_Store
|
|
12
|
+
|
|
13
|
+
# Editor
|
|
14
|
+
.idea/
|
|
15
|
+
.vscode/
|
|
16
|
+
|
|
17
|
+
# Agent-local worktrees and planning artifacts
|
|
18
|
+
/.claude/
|
|
19
|
+
/docs/superpowers/
|
|
20
|
+
|
|
21
|
+
# Local release credentials (never commit)
|
|
22
|
+
/.env.sdk-release
|
|
23
|
+
|
|
24
|
+
# Prod 외부 시드 시크릿의 로컬 원본 (never commit)
|
|
25
|
+
/infra/terraform/envs/prod/.env.secrets
|
|
26
|
+
# Terraform 로컬 계획 산출물
|
|
27
|
+
/infra/terraform/**/tfplan*
|
|
28
|
+
|
|
29
|
+
# Node / TypeScript
|
|
30
|
+
node_modules/
|
|
31
|
+
dist/
|
|
32
|
+
.astro/
|
|
33
|
+
coverage/
|
|
34
|
+
# per-package pnpm lock (workspace uses a single root lock if any)
|
|
35
|
+
packages/*/pnpm-lock.yaml
|
|
36
|
+
|
|
37
|
+
# Python
|
|
38
|
+
__pycache__/
|
|
39
|
+
*.pyc
|
|
40
|
+
*.egg-info/
|
|
41
|
+
.pytest_cache/
|
|
42
|
+
build/
|
|
43
|
+
.venv/
|
|
44
|
+
|
|
45
|
+
# 로컬 전용 compose override (시드 DB 연결)
|
|
46
|
+
docker-compose.override.yml
|
abto-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: abto
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: ABTO server-side SDK for Python — gateway baseURL + x-abto-* header injection via contextvars and httpx event hooks.
|
|
5
|
+
Project-URL: Homepage, https://github.com/greedy-co/abto
|
|
6
|
+
Project-URL: Issues, https://github.com/greedy-co/abto/issues
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Keywords: abto,analytics,autocapture,llm,observability
|
|
9
|
+
Requires-Python: >=3.9
|
|
10
|
+
Provides-Extra: httpx
|
|
11
|
+
Requires-Dist: httpx>=0.24; extra == 'httpx'
|
|
12
|
+
Provides-Extra: openai
|
|
13
|
+
Requires-Dist: httpx>=0.24; extra == 'openai'
|
|
14
|
+
Requires-Dist: openai>=1.0; extra == 'openai'
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# abto (Python)
|
|
18
|
+
|
|
19
|
+
ABTO server-side SDK for Python. 얇은 gateway 헤더 helper로, `contextvars`로 `x-abto-*` 식별자를 운반하고 `httpx` event hook으로 outbound provider 호출에 주입한다. token·cost·latency·`request_id`·variant는 gateway가 소유한다.
|
|
20
|
+
|
|
21
|
+
`@abto-app/sdk/server`(Node)의 Python 대응이다. npm과 PyPI는 다른 레지스트리라 언어별로 분리 배포한다.
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
pip install abto # 코어(contextvars 헤더 helper)
|
|
27
|
+
pip install "abto[openai]" # openai + httpx 통합까지
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Quick Start
|
|
31
|
+
|
|
32
|
+
```python
|
|
33
|
+
from abto import create_abto
|
|
34
|
+
|
|
35
|
+
abto = create_abto(
|
|
36
|
+
api_key="ABTO_API_KEY",
|
|
37
|
+
gateway_base_url="https://gateway.abto.ai/v1",
|
|
38
|
+
)
|
|
39
|
+
openai = abto.openai() # base_url=gateway + 요청마다 x-abto-* 주입
|
|
40
|
+
|
|
41
|
+
def generate(user_id: str, trace_id: str):
|
|
42
|
+
with abto.with_context(user_id=user_id, node_id="resume.make", trace_id=trace_id):
|
|
43
|
+
return openai.chat.completions.create(
|
|
44
|
+
model="gpt-4.1",
|
|
45
|
+
messages=[{"role": "user", "content": "Create a resume draft"}],
|
|
46
|
+
)
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## httpx 직접 사용
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
import httpx
|
|
53
|
+
from abto import abto_request_hook, with_context
|
|
54
|
+
|
|
55
|
+
client = httpx.Client(event_hooks={"request": [abto_request_hook()]})
|
|
56
|
+
|
|
57
|
+
with with_context(user_id="u1", node_id="resume.make"):
|
|
58
|
+
client.post("https://gateway.abto.ai/v1/...") # x-abto-* 자동 첨부
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## Public API
|
|
62
|
+
|
|
63
|
+
- `create_abto(api_key=None, gateway_base_url=None) -> Abto`
|
|
64
|
+
- `abto.openai(**kwargs)` — gateway baseURL + 헤더 주입된 OpenAI client (extra: `openai`)
|
|
65
|
+
- `abto.with_context(user_id=?, node_id=?, trace_id=?)` — 요청 단위 context (context manager)
|
|
66
|
+
- `abto.get_headers(ctx=None)` / `abto.create_trace_id()` / `abto.httpx_event_hooks()`
|
|
67
|
+
- 하위 helper: `with_context`, `get_context`, `set_context`, `get_headers`, `create_trace_id`, `create_traceparent`, `abto_request_hook`, `ABTO_HEADER`, `AbtoContext`
|
|
68
|
+
|
|
69
|
+
## Header Contract
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
x-abto-device-id (required)
|
|
73
|
+
x-abto-node-key (required; "feature.node" dot notation, e.g. resume.make)
|
|
74
|
+
traceparent (trace_id; gateway-deferred in Round1)
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Gateway는 API key를 `tenant_id`로 매핑하고 `request_id`(응답 `x-request-id`)를 발급하며 `variant_id`를 배정하고, provider 전달 전 `x-abto-*`를 strip한다.
|
|
78
|
+
|
|
79
|
+
## Test
|
|
80
|
+
|
|
81
|
+
```bash
|
|
82
|
+
pip install pytest && pytest
|
|
83
|
+
```
|
abto-0.0.1/README.md
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# abto (Python)
|
|
2
|
+
|
|
3
|
+
ABTO server-side SDK for Python. 얇은 gateway 헤더 helper로, `contextvars`로 `x-abto-*` 식별자를 운반하고 `httpx` event hook으로 outbound provider 호출에 주입한다. token·cost·latency·`request_id`·variant는 gateway가 소유한다.
|
|
4
|
+
|
|
5
|
+
`@abto-app/sdk/server`(Node)의 Python 대응이다. npm과 PyPI는 다른 레지스트리라 언어별로 분리 배포한다.
|
|
6
|
+
|
|
7
|
+
## Install
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install abto # 코어(contextvars 헤더 helper)
|
|
11
|
+
pip install "abto[openai]" # openai + httpx 통합까지
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Quick Start
|
|
15
|
+
|
|
16
|
+
```python
|
|
17
|
+
from abto import create_abto
|
|
18
|
+
|
|
19
|
+
abto = create_abto(
|
|
20
|
+
api_key="ABTO_API_KEY",
|
|
21
|
+
gateway_base_url="https://gateway.abto.ai/v1",
|
|
22
|
+
)
|
|
23
|
+
openai = abto.openai() # base_url=gateway + 요청마다 x-abto-* 주입
|
|
24
|
+
|
|
25
|
+
def generate(user_id: str, trace_id: str):
|
|
26
|
+
with abto.with_context(user_id=user_id, node_id="resume.make", trace_id=trace_id):
|
|
27
|
+
return openai.chat.completions.create(
|
|
28
|
+
model="gpt-4.1",
|
|
29
|
+
messages=[{"role": "user", "content": "Create a resume draft"}],
|
|
30
|
+
)
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## httpx 직접 사용
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import httpx
|
|
37
|
+
from abto import abto_request_hook, with_context
|
|
38
|
+
|
|
39
|
+
client = httpx.Client(event_hooks={"request": [abto_request_hook()]})
|
|
40
|
+
|
|
41
|
+
with with_context(user_id="u1", node_id="resume.make"):
|
|
42
|
+
client.post("https://gateway.abto.ai/v1/...") # x-abto-* 자동 첨부
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Public API
|
|
46
|
+
|
|
47
|
+
- `create_abto(api_key=None, gateway_base_url=None) -> Abto`
|
|
48
|
+
- `abto.openai(**kwargs)` — gateway baseURL + 헤더 주입된 OpenAI client (extra: `openai`)
|
|
49
|
+
- `abto.with_context(user_id=?, node_id=?, trace_id=?)` — 요청 단위 context (context manager)
|
|
50
|
+
- `abto.get_headers(ctx=None)` / `abto.create_trace_id()` / `abto.httpx_event_hooks()`
|
|
51
|
+
- 하위 helper: `with_context`, `get_context`, `set_context`, `get_headers`, `create_trace_id`, `create_traceparent`, `abto_request_hook`, `ABTO_HEADER`, `AbtoContext`
|
|
52
|
+
|
|
53
|
+
## Header Contract
|
|
54
|
+
|
|
55
|
+
```text
|
|
56
|
+
x-abto-device-id (required)
|
|
57
|
+
x-abto-node-key (required; "feature.node" dot notation, e.g. resume.make)
|
|
58
|
+
traceparent (trace_id; gateway-deferred in Round1)
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Gateway는 API key를 `tenant_id`로 매핑하고 `request_id`(응답 `x-request-id`)를 발급하며 `variant_id`를 배정하고, provider 전달 전 `x-abto-*`를 strip한다.
|
|
62
|
+
|
|
63
|
+
## Test
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
pip install pytest && pytest
|
|
67
|
+
```
|
|
@@ -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
|
+
]
|
|
@@ -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)
|
|
@@ -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
|