metergraph 0.4.0__tar.gz → 0.6.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.6.0/MANIFEST.in +1 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/PKG-INFO +64 -18
- {metergraph-0.4.0 → metergraph-0.6.0}/README.md +62 -17
- {metergraph-0.4.0 → metergraph-0.6.0}/pyproject.toml +2 -2
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/__init__.py +47 -5
- metergraph-0.6.0/src/metergraph/_batch_first.py +326 -0
- metergraph-0.6.0/src/metergraph/_provider_batch.py +390 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_repo_config.py +4 -4
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_session.py +1 -1
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_version.py +1 -1
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph.egg-info/PKG-INFO +64 -18
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph.egg-info/SOURCES.txt +4 -10
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph.egg-info/requires.txt +1 -0
- metergraph-0.4.0/tests/test_capture_repo_root.py +0 -101
- metergraph-0.4.0/tests/test_edge_cases.py +0 -556
- metergraph-0.4.0/tests/test_init_repo_aware.py +0 -110
- metergraph-0.4.0/tests/test_real_client_integration.py +0 -327
- metergraph-0.4.0/tests/test_repository_aware_ingest.py +0 -221
- metergraph-0.4.0/tests/test_sdk.py +0 -1465
- metergraph-0.4.0/tests/test_seam_reality.py +0 -35
- metergraph-0.4.0/tests/test_session_manager.py +0 -319
- metergraph-0.4.0/tests/test_writer_session.py +0 -142
- {metergraph-0.4.0 → metergraph-0.6.0}/setup.cfg +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_capture.py +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_config.py +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_context.py +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_failure_log.py +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_template.py +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_track.py +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph/_transport.py +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph.egg-info/dependency_links.txt +0 -0
- {metergraph-0.4.0 → metergraph-0.6.0}/src/metergraph.egg-info/top_level.txt +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
prune tests
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: metergraph
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Fire-and-forget LLM spend capture for Metergraph
|
|
5
5
|
Author: Pioneer Square Labs
|
|
6
6
|
License-Expression: Apache-2.0
|
|
@@ -14,6 +14,7 @@ Classifier: Typing :: Typed
|
|
|
14
14
|
Requires-Python: >=3.10
|
|
15
15
|
Description-Content-Type: text/markdown
|
|
16
16
|
Provides-Extra: dev
|
|
17
|
+
Requires-Dist: build>=1; extra == "dev"
|
|
17
18
|
Requires-Dist: pytest>=8; extra == "dev"
|
|
18
19
|
Requires-Dist: openai>=2.50.0; extra == "dev"
|
|
19
20
|
Requires-Dist: anthropic>=0.40; extra == "dev"
|
|
@@ -23,14 +24,18 @@ Requires-Dist: google-genai>=1; extra == "dev"
|
|
|
23
24
|
|
|
24
25
|
Zero-runtime-dependency capture for OpenAI, Anthropic, Gemini, and Python
|
|
25
26
|
Vercel AI Gateway clients.
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
Initialize Metergraph once, then wrap each provider client. `init()` reads the
|
|
28
|
+
token and other omitted options from the environment.
|
|
29
|
+
|
|
30
|
+
Initialization is process-wide. The first `init()` configuration remains
|
|
31
|
+
active; later explicit calls are ignored and produce one generic warning
|
|
32
|
+
without option names, token values, or other secrets.
|
|
29
33
|
|
|
30
34
|
```python
|
|
31
35
|
import metergraph
|
|
32
36
|
from openai import OpenAI
|
|
33
37
|
|
|
38
|
+
metergraph.init(repository="owner/repository")
|
|
34
39
|
# Anthropic() and google-genai's genai.Client() wrap the same way.
|
|
35
40
|
client = metergraph.wrap(OpenAI())
|
|
36
41
|
metergraph.set_session("ticket-123")
|
|
@@ -61,6 +66,7 @@ import os
|
|
|
61
66
|
import metergraph
|
|
62
67
|
from openai import OpenAI
|
|
63
68
|
|
|
69
|
+
metergraph.init(repository="owner/repository")
|
|
64
70
|
gateway = metergraph.wrap(OpenAI(
|
|
65
71
|
api_key=os.getenv("AI_GATEWAY_API_KEY") or os.getenv("VERCEL_OIDC_TOKEN"),
|
|
66
72
|
base_url="https://ai-gateway.vercel.sh/v1",
|
|
@@ -81,23 +87,32 @@ Configuration:
|
|
|
81
87
|
|
|
82
88
|
- `METERGRAPH_APP_TOKEN` — required bearer token
|
|
83
89
|
- `METERGRAPH_INGEST_URL` — optional override; defaults to the hosted HTTPS endpoint
|
|
90
|
+
- `METERGRAPH_REPOSITORY` — optional `owner/repository` identity; used by [MeterGraph Bot](https://github.com/apps/metergraph)
|
|
84
91
|
- `METERGRAPH_CAPTURE_TEXT=0` — opt out of content capture globally
|
|
85
92
|
- `METERGRAPH_DISABLED=1` — process kill switch
|
|
86
93
|
- `METERGRAPH_QUEUE_SIZE`, `METERGRAPH_BATCH_SIZE`, `METERGRAPH_FLUSH_SECONDS`
|
|
87
94
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
with
|
|
95
|
+
Repository identity enables repository-level attribution and
|
|
96
|
+
[MeterGraph Bot](https://github.com/apps/metergraph). Choose any one of these
|
|
97
|
+
options; each is sufficient on its own:
|
|
98
|
+
|
|
99
|
+
- Pass `repository="owner/repository"` to `metergraph.init()`.
|
|
100
|
+
- Set `METERGRAPH_REPOSITORY=owner/repository` in the environment.
|
|
101
|
+
- Store the identity with the source code in `.metergraph/config.json`:
|
|
102
|
+
|
|
103
|
+
```json
|
|
104
|
+
{"repository":"owner/repository"}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
Resolution order is the explicit option, environment variable, then
|
|
108
|
+
configuration file. The SDK treats the file as read-only. Without repository
|
|
109
|
+
identity, it warns once and continues capture without repository attribution.
|
|
95
110
|
|
|
96
111
|
Delivery is bounded and off the request path. Queue overflow or a collector
|
|
97
112
|
outage drops capture and increments internal counters; it never changes the
|
|
98
113
|
provider call. Each wire batch is bounded to 512 KiB after optional gzip.
|
|
99
|
-
|
|
100
|
-
envelope, including assistant content and tool calls
|
|
114
|
+
By default, Metergraph captures the scrubbed provider request and a normalized
|
|
115
|
+
response envelope, including assistant content and tool calls. Provider
|
|
101
116
|
credentials and transport headers are removed. Request and response are each
|
|
102
117
|
limited to 100 KiB of UTF-8 with an explicit truncation marker.
|
|
103
118
|
`capture_text=False` on `route()` or `trace()` overrides the global content
|
|
@@ -125,6 +140,35 @@ message batches are captured per inference while iterating a wrapped
|
|
|
125
140
|
rows carry real per-result usage and the batch pricing flag; job-management
|
|
126
141
|
polls themselves are not miscounted as model calls.
|
|
127
142
|
|
|
143
|
+
## Batch-first execution (opt-in)
|
|
144
|
+
|
|
145
|
+
`metergraph.batch_first()` is a separate, explicitly opt-in code path from `wrap()`/capture: submit one request through a provider's Batch API, wait up to a caller-chosen deadline, and fall back to exactly one direct call if the batch hasn't finished in time. It is synchronous/blocking, matching this SDK's own background-work model — a daemon thread, not asyncio.
|
|
146
|
+
|
|
147
|
+
```python
|
|
148
|
+
import metergraph
|
|
149
|
+
from openai import OpenAI
|
|
150
|
+
|
|
151
|
+
client = OpenAI() # unwrapped — batch_first() drives it directly, not through wrap()
|
|
152
|
+
|
|
153
|
+
outcome = metergraph.batch_first(
|
|
154
|
+
client, "openai",
|
|
155
|
+
{"model": "gpt-5-mini", "input": "Summarize this document."},
|
|
156
|
+
deadline_seconds=60,
|
|
157
|
+
accept_duplicate_provider_execution=True, # required: a missed deadline can execute the request twice
|
|
158
|
+
on_late_batch_settled=lambda info: None, # fires later, in the background, only for a losing batch
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
outcome.source # "batch" | "direct"
|
|
162
|
+
outcome.result # the provider response
|
|
163
|
+
outcome.metadata.batch_outcome # "completed" | "failed" | "expired" | "pending_at_deadline"
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`provider` is `"openai" | "anthropic" | "google"`, matching `wrap()`'s own explicit-provider option — never inferred from the client instance. A request with `stream=True` is rejected before any provider call. A request carrying `tools` is rejected unless `allow_duplicate_tool_call_plans=True` is also set, acknowledging that the batch result and the direct fallback are independent provider executions that may each choose a different tool-call plan. `accept_duplicate_provider_execution` must be exactly `True` — there is no default and no environment-variable override, and a missed deadline can execute (and bill) the same request twice. Neither the batch nor the direct path executes a tool call automatically; the caller receives the tool-call plan in `outcome.result` and is responsible for executing it, exactly as with a normal (non-batch-first) provider response.
|
|
167
|
+
|
|
168
|
+
`batch_first()` is not integrated with `wrap()`'s capture/telemetry pipeline; its result and metadata are returned directly to the caller, never enqueued for delivery. The background poll that watches a losing batch for late telemetry runs on a daemon thread, which does not keep the process alive — a short-lived script may exit before `on_late_batch_settled` ever fires, silently dropping that signal. This milestone's adapters call their provider client's methods synchronously; `AsyncOpenAI`, `AsyncAnthropic`, and google-genai's `.aio` namespace are not supported.
|
|
169
|
+
|
|
170
|
+
OpenAI, Anthropic, and Google Gemini all have adapters (`create_openai_batch_adapter`, `create_anthropic_batch_adapter`, `create_google_batch_adapter`), built against each provider's real SDK method signatures — verified by inspecting `openai`, `anthropic`, and `google-genai` as installed from this package's own dev extras, not by a live call — but **none has been exercised against a live provider Batch API from this SDK**. Treat this as a beta-quality, code-reviewed-but-not-live-verified surface.
|
|
171
|
+
|
|
128
172
|
## Set up with an AI coding agent
|
|
129
173
|
|
|
130
174
|
Paste this into Claude Code, Codex, Cursor, or any coding agent inside the
|
|
@@ -138,11 +182,13 @@ genai.Client() construction in place, e.g. client = metergraph.wrap(OpenAI()).
|
|
|
138
182
|
OpenAI or Anthropic clients pointed at https://ai-gateway.vercel.sh are Vercel
|
|
139
183
|
AI Gateway clients and are detected automatically; keep their creator/model ID
|
|
140
184
|
and AI_GATEWAY_API_KEY / VERCEL_OIDC_TOKEN configuration unchanged.
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
185
|
+
Before wrapping, call metergraph.init(repository="owner/repository") using the
|
|
186
|
+
actual GitHub owner and repository name. METERGRAPH_APP_TOKEN is required; the
|
|
187
|
+
SDK warns and disables capture when it is missing. METERGRAPH_INGEST_URL is
|
|
188
|
+
only for self-hosted servers. Document variable names with placeholders in
|
|
189
|
+
.env.example, and put real values only in deployment configuration. Metergraph
|
|
190
|
+
captures scrubbed provider requests and normalized responses by default for
|
|
191
|
+
the hosted dashboard; use
|
|
146
192
|
METERGRAPH_CAPTURE_TEXT=0 or capture_text=False around sensitive operations.
|
|
147
193
|
Provider credentials and transport headers must never be captured. Capture is
|
|
148
194
|
fail-open, so do not change call sites, arguments, or error handling; sync,
|
|
@@ -2,14 +2,18 @@
|
|
|
2
2
|
|
|
3
3
|
Zero-runtime-dependency capture for OpenAI, Anthropic, Gemini, and Python
|
|
4
4
|
Vercel AI Gateway clients.
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Initialize Metergraph once, then wrap each provider client. `init()` reads the
|
|
6
|
+
token and other omitted options from the environment.
|
|
7
|
+
|
|
8
|
+
Initialization is process-wide. The first `init()` configuration remains
|
|
9
|
+
active; later explicit calls are ignored and produce one generic warning
|
|
10
|
+
without option names, token values, or other secrets.
|
|
8
11
|
|
|
9
12
|
```python
|
|
10
13
|
import metergraph
|
|
11
14
|
from openai import OpenAI
|
|
12
15
|
|
|
16
|
+
metergraph.init(repository="owner/repository")
|
|
13
17
|
# Anthropic() and google-genai's genai.Client() wrap the same way.
|
|
14
18
|
client = metergraph.wrap(OpenAI())
|
|
15
19
|
metergraph.set_session("ticket-123")
|
|
@@ -40,6 +44,7 @@ import os
|
|
|
40
44
|
import metergraph
|
|
41
45
|
from openai import OpenAI
|
|
42
46
|
|
|
47
|
+
metergraph.init(repository="owner/repository")
|
|
43
48
|
gateway = metergraph.wrap(OpenAI(
|
|
44
49
|
api_key=os.getenv("AI_GATEWAY_API_KEY") or os.getenv("VERCEL_OIDC_TOKEN"),
|
|
45
50
|
base_url="https://ai-gateway.vercel.sh/v1",
|
|
@@ -60,23 +65,32 @@ Configuration:
|
|
|
60
65
|
|
|
61
66
|
- `METERGRAPH_APP_TOKEN` — required bearer token
|
|
62
67
|
- `METERGRAPH_INGEST_URL` — optional override; defaults to the hosted HTTPS endpoint
|
|
68
|
+
- `METERGRAPH_REPOSITORY` — optional `owner/repository` identity; used by [MeterGraph Bot](https://github.com/apps/metergraph)
|
|
63
69
|
- `METERGRAPH_CAPTURE_TEXT=0` — opt out of content capture globally
|
|
64
70
|
- `METERGRAPH_DISABLED=1` — process kill switch
|
|
65
71
|
- `METERGRAPH_QUEUE_SIZE`, `METERGRAPH_BATCH_SIZE`, `METERGRAPH_FLUSH_SECONDS`
|
|
66
72
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
with
|
|
73
|
+
Repository identity enables repository-level attribution and
|
|
74
|
+
[MeterGraph Bot](https://github.com/apps/metergraph). Choose any one of these
|
|
75
|
+
options; each is sufficient on its own:
|
|
76
|
+
|
|
77
|
+
- Pass `repository="owner/repository"` to `metergraph.init()`.
|
|
78
|
+
- Set `METERGRAPH_REPOSITORY=owner/repository` in the environment.
|
|
79
|
+
- Store the identity with the source code in `.metergraph/config.json`:
|
|
80
|
+
|
|
81
|
+
```json
|
|
82
|
+
{"repository":"owner/repository"}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Resolution order is the explicit option, environment variable, then
|
|
86
|
+
configuration file. The SDK treats the file as read-only. Without repository
|
|
87
|
+
identity, it warns once and continues capture without repository attribution.
|
|
74
88
|
|
|
75
89
|
Delivery is bounded and off the request path. Queue overflow or a collector
|
|
76
90
|
outage drops capture and increments internal counters; it never changes the
|
|
77
91
|
provider call. Each wire batch is bounded to 512 KiB after optional gzip.
|
|
78
|
-
|
|
79
|
-
envelope, including assistant content and tool calls
|
|
92
|
+
By default, Metergraph captures the scrubbed provider request and a normalized
|
|
93
|
+
response envelope, including assistant content and tool calls. Provider
|
|
80
94
|
credentials and transport headers are removed. Request and response are each
|
|
81
95
|
limited to 100 KiB of UTF-8 with an explicit truncation marker.
|
|
82
96
|
`capture_text=False` on `route()` or `trace()` overrides the global content
|
|
@@ -104,6 +118,35 @@ message batches are captured per inference while iterating a wrapped
|
|
|
104
118
|
rows carry real per-result usage and the batch pricing flag; job-management
|
|
105
119
|
polls themselves are not miscounted as model calls.
|
|
106
120
|
|
|
121
|
+
## Batch-first execution (opt-in)
|
|
122
|
+
|
|
123
|
+
`metergraph.batch_first()` is a separate, explicitly opt-in code path from `wrap()`/capture: submit one request through a provider's Batch API, wait up to a caller-chosen deadline, and fall back to exactly one direct call if the batch hasn't finished in time. It is synchronous/blocking, matching this SDK's own background-work model — a daemon thread, not asyncio.
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
import metergraph
|
|
127
|
+
from openai import OpenAI
|
|
128
|
+
|
|
129
|
+
client = OpenAI() # unwrapped — batch_first() drives it directly, not through wrap()
|
|
130
|
+
|
|
131
|
+
outcome = metergraph.batch_first(
|
|
132
|
+
client, "openai",
|
|
133
|
+
{"model": "gpt-5-mini", "input": "Summarize this document."},
|
|
134
|
+
deadline_seconds=60,
|
|
135
|
+
accept_duplicate_provider_execution=True, # required: a missed deadline can execute the request twice
|
|
136
|
+
on_late_batch_settled=lambda info: None, # fires later, in the background, only for a losing batch
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
outcome.source # "batch" | "direct"
|
|
140
|
+
outcome.result # the provider response
|
|
141
|
+
outcome.metadata.batch_outcome # "completed" | "failed" | "expired" | "pending_at_deadline"
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
`provider` is `"openai" | "anthropic" | "google"`, matching `wrap()`'s own explicit-provider option — never inferred from the client instance. A request with `stream=True` is rejected before any provider call. A request carrying `tools` is rejected unless `allow_duplicate_tool_call_plans=True` is also set, acknowledging that the batch result and the direct fallback are independent provider executions that may each choose a different tool-call plan. `accept_duplicate_provider_execution` must be exactly `True` — there is no default and no environment-variable override, and a missed deadline can execute (and bill) the same request twice. Neither the batch nor the direct path executes a tool call automatically; the caller receives the tool-call plan in `outcome.result` and is responsible for executing it, exactly as with a normal (non-batch-first) provider response.
|
|
145
|
+
|
|
146
|
+
`batch_first()` is not integrated with `wrap()`'s capture/telemetry pipeline; its result and metadata are returned directly to the caller, never enqueued for delivery. The background poll that watches a losing batch for late telemetry runs on a daemon thread, which does not keep the process alive — a short-lived script may exit before `on_late_batch_settled` ever fires, silently dropping that signal. This milestone's adapters call their provider client's methods synchronously; `AsyncOpenAI`, `AsyncAnthropic`, and google-genai's `.aio` namespace are not supported.
|
|
147
|
+
|
|
148
|
+
OpenAI, Anthropic, and Google Gemini all have adapters (`create_openai_batch_adapter`, `create_anthropic_batch_adapter`, `create_google_batch_adapter`), built against each provider's real SDK method signatures — verified by inspecting `openai`, `anthropic`, and `google-genai` as installed from this package's own dev extras, not by a live call — but **none has been exercised against a live provider Batch API from this SDK**. Treat this as a beta-quality, code-reviewed-but-not-live-verified surface.
|
|
149
|
+
|
|
107
150
|
## Set up with an AI coding agent
|
|
108
151
|
|
|
109
152
|
Paste this into Claude Code, Codex, Cursor, or any coding agent inside the
|
|
@@ -117,11 +160,13 @@ genai.Client() construction in place, e.g. client = metergraph.wrap(OpenAI()).
|
|
|
117
160
|
OpenAI or Anthropic clients pointed at https://ai-gateway.vercel.sh are Vercel
|
|
118
161
|
AI Gateway clients and are detected automatically; keep their creator/model ID
|
|
119
162
|
and AI_GATEWAY_API_KEY / VERCEL_OIDC_TOKEN configuration unchanged.
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
163
|
+
Before wrapping, call metergraph.init(repository="owner/repository") using the
|
|
164
|
+
actual GitHub owner and repository name. METERGRAPH_APP_TOKEN is required; the
|
|
165
|
+
SDK warns and disables capture when it is missing. METERGRAPH_INGEST_URL is
|
|
166
|
+
only for self-hosted servers. Document variable names with placeholders in
|
|
167
|
+
.env.example, and put real values only in deployment configuration. Metergraph
|
|
168
|
+
captures scrubbed provider requests and normalized responses by default for
|
|
169
|
+
the hosted dashboard; use
|
|
125
170
|
METERGRAPH_CAPTURE_TEXT=0 or capture_text=False around sensitive operations.
|
|
126
171
|
Provider credentials and transport headers must never be captured. Capture is
|
|
127
172
|
fail-open, so do not change call sites, arguments, or error handling; sync,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "metergraph"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.6.0"
|
|
4
4
|
description = "Fire-and-forget LLM spend capture for Metergraph"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
requires-python = ">=3.10"
|
|
@@ -20,7 +20,7 @@ Repository = "https://github.com/PioneerSquareLabs/metergraphsdk"
|
|
|
20
20
|
Issues = "https://github.com/PioneerSquareLabs/metergraphsdk/issues"
|
|
21
21
|
|
|
22
22
|
[project.optional-dependencies]
|
|
23
|
-
dev = ["pytest>=8", "openai>=2.50.0", "anthropic>=0.40", "google-genai>=1"]
|
|
23
|
+
dev = ["build>=1", "pytest>=8", "openai>=2.50.0", "anthropic>=0.40", "google-genai>=1"]
|
|
24
24
|
|
|
25
25
|
[build-system]
|
|
26
26
|
requires = ["setuptools>=68"]
|
|
@@ -14,7 +14,14 @@ from ._capture import Options, Runtime, set_runtime
|
|
|
14
14
|
from ._capture import wrap as _wrap
|
|
15
15
|
from ._config import ConfigPoller
|
|
16
16
|
from ._context import route, set_session, set_tags, snapshot, trace, wrap_executor
|
|
17
|
-
from .
|
|
17
|
+
from ._batch_first import (
|
|
18
|
+
BatchFirstIneligibleError,
|
|
19
|
+
BatchFirstMetadata,
|
|
20
|
+
BatchFirstResult,
|
|
21
|
+
LateBatchInfo,
|
|
22
|
+
batch_first,
|
|
23
|
+
)
|
|
24
|
+
from ._repo_config import RepoConfig, discover_repo_config
|
|
18
25
|
from ._session import SessionManager
|
|
19
26
|
from ._track import track
|
|
20
27
|
from ._transport import Writer
|
|
@@ -29,6 +36,8 @@ _config: ConfigPoller | None = None
|
|
|
29
36
|
_session_manager: SessionManager | None = None
|
|
30
37
|
_initialized = False
|
|
31
38
|
_warned_no_token = False
|
|
39
|
+
_warned_no_repository = False
|
|
40
|
+
_warned_repeated_init = False
|
|
32
41
|
|
|
33
42
|
|
|
34
43
|
def _env_bool(name: str, default: bool) -> bool:
|
|
@@ -38,6 +47,17 @@ def _env_bool(name: str, default: bool) -> bool:
|
|
|
38
47
|
return value.lower() not in {"0", "false", "no", "off"}
|
|
39
48
|
|
|
40
49
|
|
|
50
|
+
def _warn_repeated_init() -> None:
|
|
51
|
+
global _warned_repeated_init
|
|
52
|
+
if _warned_repeated_init:
|
|
53
|
+
return
|
|
54
|
+
_warned_repeated_init = True
|
|
55
|
+
log.warning(
|
|
56
|
+
"Metergraph init() was called more than once; "
|
|
57
|
+
"the first configuration remains active."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
41
61
|
def init(
|
|
42
62
|
*,
|
|
43
63
|
token: str | None = None,
|
|
@@ -45,13 +65,16 @@ def init(
|
|
|
45
65
|
capture_text: bool | None = None,
|
|
46
66
|
redact: Callable[[str, str], str] | None = None,
|
|
47
67
|
app_root: str | None = None,
|
|
68
|
+
repository: str | None = None,
|
|
48
69
|
skip_frames: list[str] | None = None,
|
|
49
70
|
environment: str | None = None,
|
|
50
71
|
disabled: bool | None = None,
|
|
51
72
|
) -> None:
|
|
52
73
|
"""Initialize capture. This function is idempotent and never raises."""
|
|
53
|
-
global _initialized, _warned_no_token,
|
|
74
|
+
global _initialized, _warned_no_token, _warned_no_repository
|
|
75
|
+
global _writer, _config, _session_manager
|
|
54
76
|
if _initialized:
|
|
77
|
+
_warn_repeated_init()
|
|
55
78
|
return
|
|
56
79
|
if os.getenv("METERGRAPH_DISABLED") == "1" or disabled:
|
|
57
80
|
_initialized = True
|
|
@@ -66,10 +89,23 @@ def init(
|
|
|
66
89
|
"Metergraph capture disabled: token and ingest URL are required"
|
|
67
90
|
)
|
|
68
91
|
return
|
|
69
|
-
_initialized = True
|
|
70
92
|
try:
|
|
71
93
|
app_root_resolved = os.path.realpath(app_root or os.getcwd())
|
|
72
|
-
|
|
94
|
+
repository_value = repository or os.getenv("METERGRAPH_REPOSITORY")
|
|
95
|
+
repo_config = (
|
|
96
|
+
RepoConfig(repository_value.strip(), app_root_resolved)
|
|
97
|
+
if isinstance(repository_value, str)
|
|
98
|
+
and "/" in repository_value.strip()
|
|
99
|
+
else discover_repo_config(app_root_resolved)
|
|
100
|
+
)
|
|
101
|
+
_initialized = True
|
|
102
|
+
if repo_config is None and not _warned_no_repository:
|
|
103
|
+
_warned_no_repository = True
|
|
104
|
+
log.warning(
|
|
105
|
+
"Metergraph repository identity is not configured; set "
|
|
106
|
+
"init(repository='owner/repository'), METERGRAPH_REPOSITORY, "
|
|
107
|
+
"or provide .metergraph/config.json. Continuing with legacy ingestion."
|
|
108
|
+
)
|
|
73
109
|
session = (
|
|
74
110
|
SessionManager(
|
|
75
111
|
token,
|
|
@@ -143,7 +179,8 @@ def wrap(client: Any, *, provider: str | None = None) -> Any:
|
|
|
143
179
|
force gateway handling for a compatible client with a custom URL. Call
|
|
144
180
|
init(...) first to pass Metergraph options in code.
|
|
145
181
|
"""
|
|
146
|
-
|
|
182
|
+
if not _initialized:
|
|
183
|
+
init()
|
|
147
184
|
return _wrap(client, provider=provider)
|
|
148
185
|
|
|
149
186
|
|
|
@@ -248,6 +285,11 @@ def shutdown() -> None:
|
|
|
248
285
|
|
|
249
286
|
__all__ = [
|
|
250
287
|
"DEFAULT_INGEST_URL",
|
|
288
|
+
"BatchFirstIneligibleError",
|
|
289
|
+
"BatchFirstMetadata",
|
|
290
|
+
"BatchFirstResult",
|
|
291
|
+
"LateBatchInfo",
|
|
292
|
+
"batch_first",
|
|
251
293
|
"flush",
|
|
252
294
|
"init",
|
|
253
295
|
"model_for",
|