metergraph 0.1.0__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.
- metergraph-0.1.0/PKG-INFO +88 -0
- metergraph-0.1.0/README.md +78 -0
- metergraph-0.1.0/pyproject.toml +21 -0
- metergraph-0.1.0/setup.cfg +4 -0
- metergraph-0.1.0/src/metergraph/__init__.py +219 -0
- metergraph-0.1.0/src/metergraph/_capture.py +1146 -0
- metergraph-0.1.0/src/metergraph/_config.py +144 -0
- metergraph-0.1.0/src/metergraph/_context.py +136 -0
- metergraph-0.1.0/src/metergraph/_template.py +58 -0
- metergraph-0.1.0/src/metergraph/_track.py +61 -0
- metergraph-0.1.0/src/metergraph/_transport.py +171 -0
- metergraph-0.1.0/src/metergraph/_version.py +11 -0
- metergraph-0.1.0/src/metergraph.egg-info/PKG-INFO +88 -0
- metergraph-0.1.0/src/metergraph.egg-info/SOURCES.txt +16 -0
- metergraph-0.1.0/src/metergraph.egg-info/dependency_links.txt +1 -0
- metergraph-0.1.0/src/metergraph.egg-info/requires.txt +3 -0
- metergraph-0.1.0/src/metergraph.egg-info/top_level.txt +1 -0
- metergraph-0.1.0/tests/test_sdk.py +828 -0
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: metergraph
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Fire-and-forget LLM spend capture for Metergraph
|
|
5
|
+
License-Expression: Apache-2.0
|
|
6
|
+
Requires-Python: >=3.10
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
Provides-Extra: dev
|
|
9
|
+
Requires-Dist: pytest>=8; extra == "dev"
|
|
10
|
+
|
|
11
|
+
# metergraph (Python)
|
|
12
|
+
|
|
13
|
+
Zero-runtime-dependency capture for OpenAI, Anthropic, and Gemini clients.
|
|
14
|
+
`wrap()` initializes capture from the environment, so setup is one line per
|
|
15
|
+
client; call `metergraph.init(...)` before the first `wrap()` only to pass
|
|
16
|
+
options in code.
|
|
17
|
+
|
|
18
|
+
```python
|
|
19
|
+
import metergraph
|
|
20
|
+
from openai import OpenAI
|
|
21
|
+
|
|
22
|
+
# Anthropic() and google-genai's genai.Client() wrap the same way.
|
|
23
|
+
client = metergraph.wrap(OpenAI())
|
|
24
|
+
metergraph.set_session("ticket-123")
|
|
25
|
+
|
|
26
|
+
with metergraph.route("ticket-classifier", unit="answer", capture_text=True):
|
|
27
|
+
model = metergraph.model_for("ticket-classifier", default="gpt-4.1-mini")
|
|
28
|
+
client.chat.completions.create(model=model, messages=[...])
|
|
29
|
+
|
|
30
|
+
# Emit this after the user-visible task resolves. It shares the bounded async
|
|
31
|
+
# transport and contains no prompt or output content.
|
|
32
|
+
metergraph.record_outcome(
|
|
33
|
+
"ticket-classifier",
|
|
34
|
+
model=model,
|
|
35
|
+
task_completed=True,
|
|
36
|
+
feedback_score=1,
|
|
37
|
+
turns_to_resolution=2,
|
|
38
|
+
escalated=False,
|
|
39
|
+
)
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
Configuration:
|
|
43
|
+
|
|
44
|
+
- `METERGRAPH_APP_TOKEN` — required bearer token
|
|
45
|
+
- `METERGRAPH_INGEST_URL` — optional override; defaults to the hosted HTTPS endpoint
|
|
46
|
+
- `METERGRAPH_CAPTURE_TEXT=1` — opt in to content capture globally; default is metadata-only
|
|
47
|
+
- `METERGRAPH_DISABLED=1` — process kill switch
|
|
48
|
+
- `METERGRAPH_QUEUE_SIZE`, `METERGRAPH_BATCH_SIZE`, `METERGRAPH_FLUSH_SECONDS`
|
|
49
|
+
|
|
50
|
+
Delivery is bounded and off the request path. Queue overflow or a collector
|
|
51
|
+
outage drops capture and increments internal counters; it never changes the
|
|
52
|
+
provider call. Each wire batch is bounded to 512 KiB after optional gzip.
|
|
53
|
+
`capture_text=True` or `False` on `route()` overrides the global content policy
|
|
54
|
+
for that route. Config reads are ETag-aware and fail open to the default model.
|
|
55
|
+
`record_outcome` requires a stable session ID and the model actually used so a
|
|
56
|
+
session-sticky canary can compare task completion and optional feedback,
|
|
57
|
+
turn-count, escalation, abandonment, edit-distance, and regeneration signals.
|
|
58
|
+
|
|
59
|
+
OpenAI Batch API output JSONL is captured per inference when a wrapped
|
|
60
|
+
`client.files.content()` / `retrieve_content()` result is read. Anthropic
|
|
61
|
+
message batches are captured per inference while iterating a wrapped
|
|
62
|
+
`client.messages.batches.results()` result. Run result consumption inside a
|
|
63
|
+
`route()` context so the asynchronous batch retains its product route. Batch
|
|
64
|
+
rows carry real per-result usage and the batch pricing flag; job-management
|
|
65
|
+
polls themselves are not miscounted as model calls.
|
|
66
|
+
|
|
67
|
+
## Set up with an AI coding agent
|
|
68
|
+
|
|
69
|
+
Paste this into Claude Code, Codex, Cursor, or any coding agent inside the
|
|
70
|
+
codebase you want instrumented:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
Instrument this codebase's LLM API costs with the `metergraph` PyPI package
|
|
74
|
+
(https://github.com/PioneerSquareLabs/metergraphsdk): pip install metergraph,
|
|
75
|
+
then wrap every OpenAI()/AsyncOpenAI(), Anthropic()/AsyncAnthropic(), and
|
|
76
|
+
genai.Client() construction in place, e.g. client = metergraph.wrap(OpenAI()).
|
|
77
|
+
wrap() returns the same client and initializes itself from the environment:
|
|
78
|
+
METERGRAPH_APP_TOKEN is required (capture is silently off without it) and
|
|
79
|
+
METERGRAPH_INGEST_URL is only for self-hosted servers. Add both to
|
|
80
|
+
.env.example, and never commit a real token. Capture is metadata-only (tokens,
|
|
81
|
+
latency, model, no prompt/completion content) and fail-open, so do not change
|
|
82
|
+
call sites, arguments, or error handling; sync, async, and streaming work
|
|
83
|
+
unchanged. Attribution to the calling function is automatic; optionally pin
|
|
84
|
+
stable names on key LLM-calling functions with @metergraph.track. On
|
|
85
|
+
serverless, call metergraph.flush() before the handler returns. When done,
|
|
86
|
+
list every client you wrapped and flag LLM calls made outside the official
|
|
87
|
+
openai / anthropic / google-genai SDKs, since those are not captured.
|
|
88
|
+
```
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# metergraph (Python)
|
|
2
|
+
|
|
3
|
+
Zero-runtime-dependency capture for OpenAI, Anthropic, and Gemini clients.
|
|
4
|
+
`wrap()` initializes capture from the environment, so setup is one line per
|
|
5
|
+
client; call `metergraph.init(...)` before the first `wrap()` only to pass
|
|
6
|
+
options in code.
|
|
7
|
+
|
|
8
|
+
```python
|
|
9
|
+
import metergraph
|
|
10
|
+
from openai import OpenAI
|
|
11
|
+
|
|
12
|
+
# Anthropic() and google-genai's genai.Client() wrap the same way.
|
|
13
|
+
client = metergraph.wrap(OpenAI())
|
|
14
|
+
metergraph.set_session("ticket-123")
|
|
15
|
+
|
|
16
|
+
with metergraph.route("ticket-classifier", unit="answer", capture_text=True):
|
|
17
|
+
model = metergraph.model_for("ticket-classifier", default="gpt-4.1-mini")
|
|
18
|
+
client.chat.completions.create(model=model, messages=[...])
|
|
19
|
+
|
|
20
|
+
# Emit this after the user-visible task resolves. It shares the bounded async
|
|
21
|
+
# transport and contains no prompt or output content.
|
|
22
|
+
metergraph.record_outcome(
|
|
23
|
+
"ticket-classifier",
|
|
24
|
+
model=model,
|
|
25
|
+
task_completed=True,
|
|
26
|
+
feedback_score=1,
|
|
27
|
+
turns_to_resolution=2,
|
|
28
|
+
escalated=False,
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Configuration:
|
|
33
|
+
|
|
34
|
+
- `METERGRAPH_APP_TOKEN` — required bearer token
|
|
35
|
+
- `METERGRAPH_INGEST_URL` — optional override; defaults to the hosted HTTPS endpoint
|
|
36
|
+
- `METERGRAPH_CAPTURE_TEXT=1` — opt in to content capture globally; default is metadata-only
|
|
37
|
+
- `METERGRAPH_DISABLED=1` — process kill switch
|
|
38
|
+
- `METERGRAPH_QUEUE_SIZE`, `METERGRAPH_BATCH_SIZE`, `METERGRAPH_FLUSH_SECONDS`
|
|
39
|
+
|
|
40
|
+
Delivery is bounded and off the request path. Queue overflow or a collector
|
|
41
|
+
outage drops capture and increments internal counters; it never changes the
|
|
42
|
+
provider call. Each wire batch is bounded to 512 KiB after optional gzip.
|
|
43
|
+
`capture_text=True` or `False` on `route()` overrides the global content policy
|
|
44
|
+
for that route. Config reads are ETag-aware and fail open to the default model.
|
|
45
|
+
`record_outcome` requires a stable session ID and the model actually used so a
|
|
46
|
+
session-sticky canary can compare task completion and optional feedback,
|
|
47
|
+
turn-count, escalation, abandonment, edit-distance, and regeneration signals.
|
|
48
|
+
|
|
49
|
+
OpenAI Batch API output JSONL is captured per inference when a wrapped
|
|
50
|
+
`client.files.content()` / `retrieve_content()` result is read. Anthropic
|
|
51
|
+
message batches are captured per inference while iterating a wrapped
|
|
52
|
+
`client.messages.batches.results()` result. Run result consumption inside a
|
|
53
|
+
`route()` context so the asynchronous batch retains its product route. Batch
|
|
54
|
+
rows carry real per-result usage and the batch pricing flag; job-management
|
|
55
|
+
polls themselves are not miscounted as model calls.
|
|
56
|
+
|
|
57
|
+
## Set up with an AI coding agent
|
|
58
|
+
|
|
59
|
+
Paste this into Claude Code, Codex, Cursor, or any coding agent inside the
|
|
60
|
+
codebase you want instrumented:
|
|
61
|
+
|
|
62
|
+
```text
|
|
63
|
+
Instrument this codebase's LLM API costs with the `metergraph` PyPI package
|
|
64
|
+
(https://github.com/PioneerSquareLabs/metergraphsdk): pip install metergraph,
|
|
65
|
+
then wrap every OpenAI()/AsyncOpenAI(), Anthropic()/AsyncAnthropic(), and
|
|
66
|
+
genai.Client() construction in place, e.g. client = metergraph.wrap(OpenAI()).
|
|
67
|
+
wrap() returns the same client and initializes itself from the environment:
|
|
68
|
+
METERGRAPH_APP_TOKEN is required (capture is silently off without it) and
|
|
69
|
+
METERGRAPH_INGEST_URL is only for self-hosted servers. Add both to
|
|
70
|
+
.env.example, and never commit a real token. Capture is metadata-only (tokens,
|
|
71
|
+
latency, model, no prompt/completion content) and fail-open, so do not change
|
|
72
|
+
call sites, arguments, or error handling; sync, async, and streaming work
|
|
73
|
+
unchanged. Attribution to the calling function is automatic; optionally pin
|
|
74
|
+
stable names on key LLM-calling functions with @metergraph.track. On
|
|
75
|
+
serverless, call metergraph.flush() before the handler returns. When done,
|
|
76
|
+
list every client you wrapped and flag LLM calls made outside the official
|
|
77
|
+
openai / anthropic / google-genai SDKs, since those are not captured.
|
|
78
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "metergraph"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Fire-and-forget LLM spend capture for Metergraph"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = "Apache-2.0"
|
|
8
|
+
dependencies = []
|
|
9
|
+
|
|
10
|
+
[project.optional-dependencies]
|
|
11
|
+
dev = ["pytest>=8"]
|
|
12
|
+
|
|
13
|
+
[build-system]
|
|
14
|
+
requires = ["setuptools>=68"]
|
|
15
|
+
build-backend = "setuptools.build_meta"
|
|
16
|
+
|
|
17
|
+
[tool.setuptools.packages.find]
|
|
18
|
+
where = ["src"]
|
|
19
|
+
|
|
20
|
+
[tool.pytest.ini_options]
|
|
21
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
"""Public Metergraph Python SDK."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import atexit
|
|
6
|
+
import logging
|
|
7
|
+
import os
|
|
8
|
+
import uuid
|
|
9
|
+
from datetime import datetime, timezone
|
|
10
|
+
from typing import Any, Callable
|
|
11
|
+
|
|
12
|
+
from ._capture import Options, Runtime, set_runtime
|
|
13
|
+
from ._capture import wrap as _wrap
|
|
14
|
+
from ._config import ConfigPoller
|
|
15
|
+
from ._context import route, set_session, set_tags, snapshot, wrap_executor
|
|
16
|
+
from ._track import track
|
|
17
|
+
from ._transport import Writer
|
|
18
|
+
from ._version import SDK_VERSION
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__version__ = SDK_VERSION
|
|
22
|
+
DEFAULT_INGEST_URL = "https://d2xus7mp8zdv6t.cloudfront.net"
|
|
23
|
+
log = logging.getLogger("metergraph")
|
|
24
|
+
_writer: Writer | None = None
|
|
25
|
+
_config: ConfigPoller | None = None
|
|
26
|
+
_initialized = False
|
|
27
|
+
_warned_no_token = False
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _env_bool(name: str, default: bool) -> bool:
|
|
31
|
+
value = os.getenv(name)
|
|
32
|
+
if value is None:
|
|
33
|
+
return default
|
|
34
|
+
return value.lower() not in {"0", "false", "no", "off"}
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def init(
|
|
38
|
+
*,
|
|
39
|
+
token: str | None = None,
|
|
40
|
+
ingest_url: str | None = None,
|
|
41
|
+
capture_text: bool | None = None,
|
|
42
|
+
redact: Callable[[str, str], str] | None = None,
|
|
43
|
+
app_root: str | None = None,
|
|
44
|
+
skip_frames: list[str] | None = None,
|
|
45
|
+
environment: str | None = None,
|
|
46
|
+
disabled: bool | None = None,
|
|
47
|
+
) -> None:
|
|
48
|
+
"""Initialize capture. This function is idempotent and never raises."""
|
|
49
|
+
global _initialized, _warned_no_token, _writer, _config
|
|
50
|
+
if _initialized:
|
|
51
|
+
return
|
|
52
|
+
if os.getenv("METERGRAPH_DISABLED") == "1" or disabled:
|
|
53
|
+
_initialized = True
|
|
54
|
+
return
|
|
55
|
+
token = token or os.getenv("METERGRAPH_APP_TOKEN")
|
|
56
|
+
ingest_url = ingest_url or os.getenv("METERGRAPH_INGEST_URL") or DEFAULT_INGEST_URL
|
|
57
|
+
if not token or not ingest_url:
|
|
58
|
+
# Stay uninitialized so a later init() that supplies a token succeeds.
|
|
59
|
+
if not _warned_no_token:
|
|
60
|
+
_warned_no_token = True
|
|
61
|
+
log.warning(
|
|
62
|
+
"Metergraph capture disabled: token and ingest URL are required"
|
|
63
|
+
)
|
|
64
|
+
return
|
|
65
|
+
_initialized = True
|
|
66
|
+
try:
|
|
67
|
+
_writer = Writer(
|
|
68
|
+
token,
|
|
69
|
+
ingest_url,
|
|
70
|
+
queue_size=int(os.getenv("METERGRAPH_QUEUE_SIZE", "2000")),
|
|
71
|
+
batch_size=int(os.getenv("METERGRAPH_BATCH_SIZE", "100")),
|
|
72
|
+
flush_seconds=float(os.getenv("METERGRAPH_FLUSH_SECONDS", "5")),
|
|
73
|
+
)
|
|
74
|
+
options = Options(
|
|
75
|
+
capture_text=(
|
|
76
|
+
_env_bool("METERGRAPH_CAPTURE_TEXT", False)
|
|
77
|
+
if capture_text is None
|
|
78
|
+
else capture_text
|
|
79
|
+
),
|
|
80
|
+
redact=redact,
|
|
81
|
+
app_root=os.path.realpath(app_root or os.getcwd()),
|
|
82
|
+
skip_frames=tuple(skip_frames or ()),
|
|
83
|
+
environment=environment or os.getenv("METERGRAPH_ENV"),
|
|
84
|
+
text_max_bytes=int(os.getenv("METERGRAPH_TEXT_MAX_BYTES", "100000")),
|
|
85
|
+
)
|
|
86
|
+
set_runtime(Runtime(_writer, options))
|
|
87
|
+
_config = ConfigPoller(
|
|
88
|
+
token,
|
|
89
|
+
ingest_url,
|
|
90
|
+
poll_seconds=float(os.getenv("METERGRAPH_CONFIG_POLL_SECONDS", "30")),
|
|
91
|
+
hard_ttl_seconds=float(
|
|
92
|
+
os.getenv("METERGRAPH_CONFIG_HARD_TTL_SECONDS", "120")
|
|
93
|
+
),
|
|
94
|
+
)
|
|
95
|
+
atexit.register(shutdown)
|
|
96
|
+
except Exception:
|
|
97
|
+
set_runtime(None)
|
|
98
|
+
if _writer:
|
|
99
|
+
_writer.shutdown()
|
|
100
|
+
_writer = None
|
|
101
|
+
_config = None
|
|
102
|
+
log.warning(
|
|
103
|
+
"Metergraph initialization failed; application is running uninstrumented"
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def wrap(client: Any, *, provider: str | None = None) -> Any:
|
|
108
|
+
"""Wrap an OpenAI, Anthropic, or Google client for capture.
|
|
109
|
+
|
|
110
|
+
Calls init() automatically, so with env-var configuration this is the
|
|
111
|
+
only setup line needed. Call init(...) first to pass options in code.
|
|
112
|
+
"""
|
|
113
|
+
init()
|
|
114
|
+
return _wrap(client, provider=provider)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def model_for(route_name: str, *, default: str, session_key: str | None = None) -> str:
|
|
118
|
+
"""Return a sticky canary model, or the incumbent on every failure path."""
|
|
119
|
+
if _config is None:
|
|
120
|
+
return default
|
|
121
|
+
return _config.model_for(route_name, default, session_key or snapshot().session_id)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def record_outcome(
|
|
125
|
+
route_name: str,
|
|
126
|
+
*,
|
|
127
|
+
model: str,
|
|
128
|
+
task_completed: bool,
|
|
129
|
+
session_key: str | None = None,
|
|
130
|
+
feedback_score: float | None = None,
|
|
131
|
+
turns_to_resolution: int | None = None,
|
|
132
|
+
escalated: bool | None = None,
|
|
133
|
+
abandoned: bool | None = None,
|
|
134
|
+
edit_distance_ratio: float | None = None,
|
|
135
|
+
regeneration_count: int | None = None,
|
|
136
|
+
event_id: str | None = None,
|
|
137
|
+
) -> bool:
|
|
138
|
+
"""Enqueue a content-free real outcome without touching the request path."""
|
|
139
|
+
if _writer is None or not isinstance(task_completed, bool):
|
|
140
|
+
return False
|
|
141
|
+
route_name = str(route_name).strip()[:512]
|
|
142
|
+
model = str(model).strip()[:512]
|
|
143
|
+
session_key = str(session_key or snapshot().session_id or "").strip()[:512]
|
|
144
|
+
event_id = str(event_id or uuid.uuid4()).strip()[:128]
|
|
145
|
+
try:
|
|
146
|
+
feedback_score = float(feedback_score) if feedback_score is not None else None
|
|
147
|
+
turns_to_resolution = (
|
|
148
|
+
int(turns_to_resolution) if turns_to_resolution is not None else None
|
|
149
|
+
)
|
|
150
|
+
edit_distance_ratio = (
|
|
151
|
+
float(edit_distance_ratio) if edit_distance_ratio is not None else None
|
|
152
|
+
)
|
|
153
|
+
regeneration_count = (
|
|
154
|
+
int(regeneration_count) if regeneration_count is not None else None
|
|
155
|
+
)
|
|
156
|
+
except (TypeError, ValueError, OverflowError):
|
|
157
|
+
return False
|
|
158
|
+
if not route_name or not model or not session_key or not event_id:
|
|
159
|
+
return False
|
|
160
|
+
if feedback_score is not None and not -1 <= feedback_score <= 1:
|
|
161
|
+
return False
|
|
162
|
+
if turns_to_resolution is not None and not 1 <= turns_to_resolution <= 1_000_000:
|
|
163
|
+
return False
|
|
164
|
+
if edit_distance_ratio is not None and not 0 <= edit_distance_ratio <= 1:
|
|
165
|
+
return False
|
|
166
|
+
if regeneration_count is not None and not 0 <= regeneration_count <= 1_000_000:
|
|
167
|
+
return False
|
|
168
|
+
if escalated is not None and not isinstance(escalated, bool):
|
|
169
|
+
return False
|
|
170
|
+
if abandoned is not None and not isinstance(abandoned, bool):
|
|
171
|
+
return False
|
|
172
|
+
return _writer.enqueue(
|
|
173
|
+
{
|
|
174
|
+
"event_type": "outcome",
|
|
175
|
+
"event_id": event_id,
|
|
176
|
+
"ts": datetime.now(timezone.utc).isoformat(),
|
|
177
|
+
"route": route_name,
|
|
178
|
+
"session_id": session_key,
|
|
179
|
+
"model": model,
|
|
180
|
+
"task_completed": task_completed,
|
|
181
|
+
"feedback_score": feedback_score,
|
|
182
|
+
"turns_to_resolution": turns_to_resolution,
|
|
183
|
+
"escalated": escalated,
|
|
184
|
+
"abandoned": abandoned,
|
|
185
|
+
"edit_distance_ratio": edit_distance_ratio,
|
|
186
|
+
"regeneration_count": regeneration_count,
|
|
187
|
+
}
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def flush(timeout: float = 3.0) -> bool:
|
|
192
|
+
return True if _writer is None else _writer.flush(timeout)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def shutdown() -> None:
|
|
196
|
+
global _writer, _config
|
|
197
|
+
if _config:
|
|
198
|
+
_config.stop()
|
|
199
|
+
_config = None
|
|
200
|
+
if _writer:
|
|
201
|
+
_writer.shutdown()
|
|
202
|
+
_writer = None
|
|
203
|
+
set_runtime(None)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
__all__ = [
|
|
207
|
+
"DEFAULT_INGEST_URL",
|
|
208
|
+
"flush",
|
|
209
|
+
"init",
|
|
210
|
+
"model_for",
|
|
211
|
+
"record_outcome",
|
|
212
|
+
"route",
|
|
213
|
+
"set_session",
|
|
214
|
+
"set_tags",
|
|
215
|
+
"shutdown",
|
|
216
|
+
"track",
|
|
217
|
+
"wrap",
|
|
218
|
+
"wrap_executor",
|
|
219
|
+
]
|