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 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