agent-first-data 0.16.0__tar.gz → 0.17.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.
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/PKG-INFO +2 -2
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/README.md +1 -1
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/__init__.py +4 -2
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/assets/protocol-v1.schema.json +2 -19
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/assets/registry.json +2 -11
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/assets/skills/agent-first-data/references/registry.json +2 -11
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/assets/skills/agent-first-data/references/rules.md +13 -12
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/cli.py +4 -10
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/format.py +77 -192
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data.egg-info/PKG-INFO +2 -2
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/pyproject.toml +1 -1
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/tests/test_cli.py +10 -3
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/tests/test_format.py +17 -16
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/assets/skills/agent-first-data/SKILL.md +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/assets/skills/agent-first-data/references/protocol-v1.schema.json +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/skill.py +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/stream_redirect.py +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data.egg-info/SOURCES.txt +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/setup.cfg +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/tests/test_no_stderr_policy.py +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/tests/test_skill.py +0 -0
- {agent_first_data-0.16.0 → agent_first_data-0.17.0}/tests/test_stream_redirect.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.0
|
|
4
4
|
Summary: A naming convention that lets AI agents understand your data without being told what it means.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
|
|
@@ -28,7 +28,7 @@ print(output_json(value))
|
|
|
28
28
|
print(output_plain(value))
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Useful names use Python casing: `output_json`, `output_yaml`, `output_plain` (each takes an optional keyword-only `options=`), `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `
|
|
31
|
+
Useful names use Python casing: `output_json`, `output_yaml`, `output_plain` (each takes an optional keyword-only `options=`), `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
|
|
32
32
|
|
|
33
33
|
Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
|
|
34
34
|
|
|
@@ -19,7 +19,7 @@ print(output_json(value))
|
|
|
19
19
|
print(output_plain(value))
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Useful names use Python casing: `output_json`, `output_yaml`, `output_plain` (each takes an optional keyword-only `options=`), `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `
|
|
22
|
+
Useful names use Python casing: `output_json`, `output_yaml`, `output_plain` (each takes an optional keyword-only `options=`), `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
|
|
23
23
|
|
|
24
24
|
Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
|
|
25
25
|
|
|
@@ -28,8 +28,9 @@ from agent_first_data.format import (
|
|
|
28
28
|
output_plain,
|
|
29
29
|
redacted_value,
|
|
30
30
|
redact_url_secrets,
|
|
31
|
-
parse_size,
|
|
32
31
|
normalize_utc_offset,
|
|
32
|
+
is_valid_bcp47,
|
|
33
|
+
is_valid_rfc3339,
|
|
33
34
|
is_valid_rfc3339_date,
|
|
34
35
|
is_valid_rfc3339_time,
|
|
35
36
|
)
|
|
@@ -75,8 +76,9 @@ __all__ = [
|
|
|
75
76
|
"output_plain",
|
|
76
77
|
"redacted_value",
|
|
77
78
|
"redact_url_secrets",
|
|
78
|
-
"parse_size",
|
|
79
79
|
"normalize_utc_offset",
|
|
80
|
+
"is_valid_bcp47",
|
|
81
|
+
"is_valid_rfc3339",
|
|
80
82
|
"is_valid_rfc3339_date",
|
|
81
83
|
"is_valid_rfc3339_time",
|
|
82
84
|
"OutputFormat",
|
{agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data/assets/protocol-v1.schema.json
RENAMED
|
@@ -47,13 +47,7 @@
|
|
|
47
47
|
"type": "object",
|
|
48
48
|
"properties": {
|
|
49
49
|
"kind": {"const": "progress"},
|
|
50
|
-
"progress":
|
|
51
|
-
"type": "object",
|
|
52
|
-
"properties": {
|
|
53
|
-
"message": {"type": "string", "minLength": 1}
|
|
54
|
-
},
|
|
55
|
-
"additionalProperties": true
|
|
56
|
-
},
|
|
50
|
+
"progress": true,
|
|
57
51
|
"trace": {"$ref": "#/$defs/trace"}
|
|
58
52
|
},
|
|
59
53
|
"required": ["kind", "progress"],
|
|
@@ -63,18 +57,7 @@
|
|
|
63
57
|
"type": "object",
|
|
64
58
|
"properties": {
|
|
65
59
|
"kind": {"const": "log"},
|
|
66
|
-
"log":
|
|
67
|
-
"type": "object",
|
|
68
|
-
"properties": {
|
|
69
|
-
"message": {"type": "string", "minLength": 1},
|
|
70
|
-
"level": {"enum": ["debug", "info", "warn", "error"]},
|
|
71
|
-
"code": false
|
|
72
|
-
},
|
|
73
|
-
"not": {
|
|
74
|
-
"required": ["code"]
|
|
75
|
-
},
|
|
76
|
-
"additionalProperties": true
|
|
77
|
-
},
|
|
60
|
+
"log": true,
|
|
78
61
|
"trace": {"$ref": "#/$defs/trace"}
|
|
79
62
|
},
|
|
80
63
|
"required": ["kind", "log"],
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"unit": "bcp47_language_tag",
|
|
133
133
|
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
134
134
|
"redaction": {"mode": "none"},
|
|
135
|
-
"constraints": {"
|
|
135
|
+
"constraints": {"bcp47": true}
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"suffix": "_utc_offset",
|
|
@@ -152,18 +152,9 @@
|
|
|
152
152
|
"redaction": {"mode": "none"},
|
|
153
153
|
"constraints": {"integer": true, "minimum": 0}
|
|
154
154
|
},
|
|
155
|
-
{
|
|
156
|
-
"suffix": "_size",
|
|
157
|
-
"category": "size",
|
|
158
|
-
"json_types": ["string"],
|
|
159
|
-
"unit": "byte_size_input",
|
|
160
|
-
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
161
|
-
"redaction": {"mode": "none"},
|
|
162
|
-
"constraints": {"parse_size_units": ["B", "kB", "MB", "GB", "TB", "KiB", "MiB", "GiB", "TiB"]}
|
|
163
|
-
},
|
|
164
155
|
{
|
|
165
156
|
"suffix": "_percent",
|
|
166
|
-
"category": "
|
|
157
|
+
"category": "percentage",
|
|
167
158
|
"json_types": ["number"],
|
|
168
159
|
"unit": "percent",
|
|
169
160
|
"formatting": {"strip_key": true, "plain_yaml": "append_percent"},
|
|
@@ -132,7 +132,7 @@
|
|
|
132
132
|
"unit": "bcp47_language_tag",
|
|
133
133
|
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
134
134
|
"redaction": {"mode": "none"},
|
|
135
|
-
"constraints": {"
|
|
135
|
+
"constraints": {"bcp47": true}
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"suffix": "_utc_offset",
|
|
@@ -152,18 +152,9 @@
|
|
|
152
152
|
"redaction": {"mode": "none"},
|
|
153
153
|
"constraints": {"integer": true, "minimum": 0}
|
|
154
154
|
},
|
|
155
|
-
{
|
|
156
|
-
"suffix": "_size",
|
|
157
|
-
"category": "size",
|
|
158
|
-
"json_types": ["string"],
|
|
159
|
-
"unit": "byte_size_input",
|
|
160
|
-
"formatting": {"strip_key": false, "plain_yaml": "pass_through"},
|
|
161
|
-
"redaction": {"mode": "none"},
|
|
162
|
-
"constraints": {"parse_size_units": ["B", "kB", "MB", "GB", "TB", "KiB", "MiB", "GiB", "TiB"]}
|
|
163
|
-
},
|
|
164
155
|
{
|
|
165
156
|
"suffix": "_percent",
|
|
166
|
-
"category": "
|
|
157
|
+
"category": "percentage",
|
|
167
158
|
"json_types": ["number"],
|
|
168
159
|
"unit": "percent",
|
|
169
160
|
"formatting": {"strip_key": true, "plain_yaml": "append_percent"},
|
|
@@ -37,6 +37,10 @@ The field name is the schema. Always encode units and semantics in the field nam
|
|
|
37
37
|
| `_epoch_ns` | nanoseconds since Unix epoch as a decimal string | `created_epoch_ns: "1707868800000000000"` |
|
|
38
38
|
| `_rfc3339` | RFC 3339 date-time string | `expires_rfc3339: "2026-02-14T10:30:00Z"` |
|
|
39
39
|
|
|
40
|
+
Integer vs string is not uniform across siblings: `_epoch_s` and `_epoch_ms` are JSON integers, but `_epoch_ns` is a decimal **string** because nanoseconds exceed the 2⁵³−1 safe-integer range. The same rule applies to large `_sats`/`_msats` (integer when safe, decimal string when not). Writing `_epoch_ns` as a bare JSON number silently loses precision — `afdata lint` rejects it (`suffix_type_mismatch`).
|
|
41
|
+
|
|
42
|
+
`_rfc3339` needs a **mandatory** offset: `YYYY-MM-DDThh:mm:ss[.fff]` followed by `Z` or `±HH:MM`. A bare `2026-02-14T10:30:00` (no offset), a space instead of `T`, or a trailing IANA name (`...Asia/Shanghai`) is rejected by `afdata lint`. `afdata lint` also type-checks the numeric suffixes (durations `_s`/`_ms`/…, currency `_cents`/`_micro`/`_jpy`) and rejects a `_url` with internal whitespace or bare `user:pass@host` credentials.
|
|
43
|
+
|
|
40
44
|
### Strict string formats
|
|
41
45
|
|
|
42
46
|
| Suffix | Format | Example |
|
|
@@ -46,7 +50,7 @@ The field name is the schema. Always encode units and semantics in the field nam
|
|
|
46
50
|
| `_rfc3339_date` | RFC 3339 full-date string | `invoice_due_rfc3339_date: "2026-06-13"` |
|
|
47
51
|
| `_rfc3339_time` | RFC 3339 partial-time string | `market_open_rfc3339_time: "09:30:00"` |
|
|
48
52
|
|
|
49
|
-
`*_bcp47` identifies a BCP-47 language tag string. AFDATA does not
|
|
53
|
+
`*_bcp47` identifies a BCP-47 language tag string. AFDATA validates structure (hyphen-separated subtags, 2–3 letter primary language), so the POSIX form `zh_CN` and an over-long primary like `chinese` are rejected; use `zh-CN`. It does not check the IANA registry, so a well-formed-but-unregistered tag like `zz-ZZ` still passes. `is_valid_bcp47` and `afdata lint` apply this check.
|
|
50
54
|
|
|
51
55
|
`*_utc_offset` identifies a fixed UTC offset. Canonical persisted and structured output values are `"UTC"` or `±HH:MM`, with `HH` in `00..23` and `MM` in `00..59`; zero offsets normalize to `"UTC"`. This is not an IANA timezone name, DST rule, or timezone database field.
|
|
52
56
|
|
|
@@ -63,11 +67,8 @@ Avoid magic string sentinels such as `"auto"` inside strict-format fields. If a
|
|
|
63
67
|
| Suffix | Example |
|
|
64
68
|
|:-------|:--------|
|
|
65
69
|
| `_bytes` | `payload_bytes: 456789` (non-negative integer) |
|
|
66
|
-
| `_size` | `buffer_size: "10MiB"` or `"10MB"` (config files only, human-readable) |
|
|
67
70
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
`parse_size("10MiB")` → `10485760`; `parse_size("10MB")` → `10000000`. Returns null for invalid or negative input.
|
|
71
|
+
Byte sizes are always integer `_bytes`, in config and output alike. There is no unit-in-value size string: never write `buffer_size: "10MiB"`. Encode the unit in the key (`buffer_bytes: 10485760`), the same way durations use `timeout_s: 30`.
|
|
71
72
|
|
|
72
73
|
### Percentage
|
|
73
74
|
|
|
@@ -75,6 +76,8 @@ Avoid magic string sentinels such as `"auto"` inside strict-format fields. If a
|
|
|
75
76
|
|:-------|:--------|
|
|
76
77
|
| `_percent` | `cpu_percent: 85` |
|
|
77
78
|
|
|
79
|
+
Value is in units of percent: `1` = 1%, `0.2` = 0.2%, `85` = 85%. Decimals, negatives, and values >100 are all valid; no fixed range. `%` is exactly 0.01, so `_percent` is never a 0–1 fraction — if the underlying ratio is `0.999`, multiply by 100 and write `success_percent: 99.9`. Writing `0.85` for 85% is a producer-side conversion bug, not a convention ambiguity.
|
|
80
|
+
|
|
78
81
|
### Currency
|
|
79
82
|
|
|
80
83
|
Bitcoin:
|
|
@@ -150,7 +153,6 @@ ORM struct fields preserve the suffix: `duration_ms: i64`, not `duration: i64`.
|
|
|
150
153
|
| `latency: 142` | `latency_ms: 142` | seconds? milliseconds? |
|
|
151
154
|
| `api_key: "sk-..."` | `api_key_secret: "sk-..."` | won't be auto-redacted |
|
|
152
155
|
| `cpu: 85` | `cpu_percent: 85` | 85 what? |
|
|
153
|
-
| `buffer: "10MiB"` | `buffer_size: "10MiB"` | only `_size` gets parsed |
|
|
154
156
|
|
|
155
157
|
---
|
|
156
158
|
|
|
@@ -174,7 +176,7 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
|
|
|
174
176
|
4. `_msats`, `_sats`, `_bytes`, `_percent`, `_secret`
|
|
175
177
|
5. `_jpy`, `_ns`, `_us`, `_ms`, `_s`
|
|
176
178
|
|
|
177
|
-
`
|
|
179
|
+
`_bcp47`, `_utc_offset`, `_rfc3339_date`, and `_rfc3339_time` are NOT stripped (pass through). If two keys collide after stripping, both revert to original key AND raw value (no formatting). Redaction runs before collision handling, so fallback never restores a secret.
|
|
178
180
|
|
|
179
181
|
### Value formatting (YAML and Plain)
|
|
180
182
|
|
|
@@ -184,7 +186,6 @@ Remove recognized formatting suffix from key. Longest match first, exact lowerca
|
|
|
184
186
|
- `_epoch_ms`/`_epoch_s`/decimal-string `_epoch_ns` → RFC 3339 (negative = pre-1970)
|
|
185
187
|
- `_rfc3339` → pass through
|
|
186
188
|
- `_bytes` → human-readable (`456789` → `446.1KiB`); negative and fractional byte values fall through raw
|
|
187
|
-
- `_size` → pass through
|
|
188
189
|
- `_percent` → append `%`
|
|
189
190
|
- `_msats` → `{n}msats`, `_sats` → `{n}sats`
|
|
190
191
|
- `_usd_cents` → `$X.XX`, `_eur_cents` → `€X.XX`, `_jpy` → `¥X,XXX`, `_{code}_cents` → `X.XX CODE`, `_{code}_micro` → `X.XXXXXX CODE` where `code` is 3-4 ASCII letters
|
|
@@ -258,7 +259,7 @@ Startup payload fields are tool-defined; `config` is recommended, while `version
|
|
|
258
259
|
|
|
259
260
|
All use `kind` plus the same-named payload and optional `trace`. Do not split protocol events across `stdout` and `stderr`.
|
|
260
261
|
|
|
261
|
-
Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event
|
|
262
|
+
Base validators enforce mandatory envelope/error/lifecycle rules. Strict validators additionally require object-valued `trace` on every event and `retryable` on every error. Log and progress payloads remain tool-defined in both profiles. Use strict validation before claiming compliance with all recommendations.
|
|
262
263
|
|
|
263
264
|
---
|
|
264
265
|
|
|
@@ -298,11 +299,11 @@ let span = info_span!("request", request_id = %uuid);
|
|
|
298
299
|
let _guard = span.enter();
|
|
299
300
|
```
|
|
300
301
|
|
|
301
|
-
For Go, Python, and TypeScript: emit span fields explicitly in each log
|
|
302
|
+
For Go, Python, and TypeScript: emit span fields explicitly in each tool-defined log payload.
|
|
302
303
|
|
|
303
304
|
### Output fields
|
|
304
305
|
|
|
305
|
-
|
|
306
|
+
Log payloads have no required or reserved fields. Traditional logging adapters commonly add `message` and `level` (debug/info/warn/error), plus span and event fields, but those names and meanings are adapter conventions rather than AFDATA protocol requirements. Projects that need timestamps add `timestamp_epoch_ms` explicitly.
|
|
306
307
|
|
|
307
308
|
Log redaction is **by field name** (the same `_secret`/`_url` rule as all output), applied when the line is emitted. Name the secret field — `info!(api_key_secret = %key)` — rather than logging a whole object by its `Debug`/string rendering, which hides the inner field names from redaction. For structured/nested secret-bearing data, build a value, apply redaction through output formatters or redacted_value(), then emit via the standard formatting helpers — do not pass the struct to a `?`/`%`-rendered log field.
|
|
308
309
|
|
|
@@ -340,7 +341,7 @@ Use this checklist when the skill is evaluating a change, not just documenting c
|
|
|
340
341
|
3. Require secret-bearing values to be named with `_secret` or configured exact secret names before serialization. Do not rely on arbitrary free-text scanning or `Debug`/string-rendered objects.
|
|
341
342
|
4. Run `afdata lint` for JSON/JSONL samples, JSON Schema, MCP schemas, or serialized outputs when available; run `afdata validate` for finite protocol events/streams. If unavailable, report the skipped check.
|
|
342
343
|
5. Before changing an external project's public API, wire format, database schema, or persisted field names, report the compatibility impact and request approval before editing.
|
|
343
|
-
6.
|
|
344
|
+
6. Byte sizes use integer `_bytes` everywhere, config included (`buffer_bytes: 10485760`, never `buffer_size: "10MiB"`)
|
|
344
345
|
7. Environment variables follow `UPPER_SNAKE_CASE` with the same suffixes
|
|
345
346
|
8. One-time CLI output uses `json_log()`/`CliEmitter` + output helpers; long-running services use `afdata_tracing::try_init()` (Rust) to capture via tracing; other languages emit log events explicitly via builders
|
|
346
347
|
9. Database columns use AFDATA suffixes on generic types (`duration_ms INTEGER`, not `duration INTEGER`); native types like `TIMESTAMPTZ` don't need suffixes
|
|
@@ -168,14 +168,8 @@ class CliEmitter:
|
|
|
168
168
|
# Apply log fields provider if this is a log event
|
|
169
169
|
if kind == "log" and self._log_fields_provider is not None:
|
|
170
170
|
provider_fields = self._log_fields_provider()
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
log_payload = envelope.get("log", {})
|
|
174
|
-
for reserved in ("message", "level", "code"):
|
|
175
|
-
if reserved in provider_fields:
|
|
176
|
-
raise RuntimeError(
|
|
177
|
-
f"log fields provider cannot write reserved field '{reserved}'"
|
|
178
|
-
)
|
|
171
|
+
log_payload = envelope.get("log")
|
|
172
|
+
if provider_fields and isinstance(log_payload, dict):
|
|
179
173
|
# Merge provider fields, explicit fields take precedence
|
|
180
174
|
merged_log = dict(provider_fields)
|
|
181
175
|
merged_log.update(log_payload)
|
|
@@ -211,7 +205,7 @@ class CliEmitter:
|
|
|
211
205
|
if not message or not isinstance(message, str):
|
|
212
206
|
raise ValueError("message must be a non-empty string")
|
|
213
207
|
# Use the builder to get default trace: {}
|
|
214
|
-
event = json_progress(message).build()
|
|
208
|
+
event = json_progress({"message": message}).build()
|
|
215
209
|
self.emit(event)
|
|
216
210
|
|
|
217
211
|
def emit_log(self, level: LogLevel | str, message: str) -> None:
|
|
@@ -220,7 +214,7 @@ class CliEmitter:
|
|
|
220
214
|
level = LogLevel(level)
|
|
221
215
|
if not message or not isinstance(message, str):
|
|
222
216
|
raise ValueError("message must be a non-empty string")
|
|
223
|
-
event = json_log(level, message).build()
|
|
217
|
+
event = json_log({"level": level.value, "message": message}).build()
|
|
224
218
|
self.emit(event)
|
|
225
219
|
|
|
226
220
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Protocol builders, value redactors (copy and in-place; cover _secret and
|
|
4
4
|
_url fields), output formatters, URL-string redactors (redact_url_secrets),
|
|
5
|
-
|
|
5
|
+
normalize_utc_offset, is_valid_rfc3339_date,
|
|
6
6
|
is_valid_rfc3339_time, RedactionPolicy, OutputStyle, and
|
|
7
7
|
OutputOptions. Each formatter/redactor concept is a single function taking
|
|
8
8
|
a keyword-only ``options`` parameter.
|
|
@@ -183,50 +183,11 @@ class ErrorBuilder:
|
|
|
183
183
|
class ProgressBuilder:
|
|
184
184
|
"""Fluent builder for progress events."""
|
|
185
185
|
|
|
186
|
-
def __init__(self,
|
|
187
|
-
self.
|
|
186
|
+
def __init__(self, payload: Any) -> None:
|
|
187
|
+
self._payload = payload
|
|
188
188
|
self._trace: dict = {}
|
|
189
|
-
self._fields: dict[str, Any] = {}
|
|
190
189
|
self._errors: list[str] = []
|
|
191
190
|
|
|
192
|
-
def field(self, name: str, value: Any) -> ProgressBuilder:
|
|
193
|
-
"""Add a single extension field."""
|
|
194
|
-
if name == "message":
|
|
195
|
-
self._errors.append(f"cannot override reserved field '{name}'")
|
|
196
|
-
else:
|
|
197
|
-
self._fields[name] = value
|
|
198
|
-
return self
|
|
199
|
-
|
|
200
|
-
def fields(self, mapping: Mapping[str, Any]) -> ProgressBuilder:
|
|
201
|
-
"""Add multiple extension fields from a mapping."""
|
|
202
|
-
if not isinstance(mapping, Mapping):
|
|
203
|
-
self._errors.append("fields must be a mapping")
|
|
204
|
-
else:
|
|
205
|
-
for k, v in mapping.items():
|
|
206
|
-
if k == "message":
|
|
207
|
-
self._errors.append(f"cannot override reserved field '{k}'")
|
|
208
|
-
else:
|
|
209
|
-
self._fields[k] = v
|
|
210
|
-
return self
|
|
211
|
-
|
|
212
|
-
def extend(self, value: Any) -> ProgressBuilder:
|
|
213
|
-
"""Add extension fields from a dataclass or mapping."""
|
|
214
|
-
if hasattr(value, "__dataclass_fields__"):
|
|
215
|
-
from dataclasses import asdict
|
|
216
|
-
mapping = asdict(value)
|
|
217
|
-
elif isinstance(value, Mapping):
|
|
218
|
-
mapping = value
|
|
219
|
-
else:
|
|
220
|
-
self._errors.append("extend() requires a dataclass or mapping")
|
|
221
|
-
return self
|
|
222
|
-
|
|
223
|
-
for k, v in mapping.items():
|
|
224
|
-
if k == "message":
|
|
225
|
-
self._errors.append(f"cannot override reserved field '{k}'")
|
|
226
|
-
else:
|
|
227
|
-
self._fields[k] = v
|
|
228
|
-
return self
|
|
229
|
-
|
|
230
191
|
def trace(self, obj: Any) -> ProgressBuilder:
|
|
231
192
|
"""Set trace context."""
|
|
232
193
|
if not isinstance(obj, dict):
|
|
@@ -239,11 +200,9 @@ class ProgressBuilder:
|
|
|
239
200
|
"""Build and return the Event, raising EventBuildError if there are errors."""
|
|
240
201
|
if self._errors:
|
|
241
202
|
raise EventBuildError("; ".join(self._errors))
|
|
242
|
-
progress = {"message": self._message}
|
|
243
|
-
progress.update(self._fields)
|
|
244
203
|
envelope = {
|
|
245
204
|
"kind": "progress",
|
|
246
|
-
"progress":
|
|
205
|
+
"progress": self._payload,
|
|
247
206
|
"trace": self._trace,
|
|
248
207
|
}
|
|
249
208
|
return Event(envelope)
|
|
@@ -252,53 +211,11 @@ class ProgressBuilder:
|
|
|
252
211
|
class LogBuilder:
|
|
253
212
|
"""Fluent builder for log events."""
|
|
254
213
|
|
|
255
|
-
def __init__(self,
|
|
256
|
-
|
|
257
|
-
level = LogLevel(level)
|
|
258
|
-
self._level = level
|
|
259
|
-
self._message = message
|
|
214
|
+
def __init__(self, payload: Any) -> None:
|
|
215
|
+
self._payload = payload
|
|
260
216
|
self._trace: dict = {}
|
|
261
|
-
self._fields: dict[str, Any] = {}
|
|
262
217
|
self._errors: list[str] = []
|
|
263
218
|
|
|
264
|
-
def field(self, name: str, value: Any) -> LogBuilder:
|
|
265
|
-
"""Add a single extension field."""
|
|
266
|
-
if name in ("message", "level", "code"):
|
|
267
|
-
self._errors.append(f"cannot override reserved field '{name}'")
|
|
268
|
-
else:
|
|
269
|
-
self._fields[name] = value
|
|
270
|
-
return self
|
|
271
|
-
|
|
272
|
-
def fields(self, mapping: Mapping[str, Any]) -> LogBuilder:
|
|
273
|
-
"""Add multiple extension fields from a mapping."""
|
|
274
|
-
if not isinstance(mapping, Mapping):
|
|
275
|
-
self._errors.append("fields must be a mapping")
|
|
276
|
-
else:
|
|
277
|
-
for k, v in mapping.items():
|
|
278
|
-
if k in ("message", "level", "code"):
|
|
279
|
-
self._errors.append(f"cannot override reserved field '{k}'")
|
|
280
|
-
else:
|
|
281
|
-
self._fields[k] = v
|
|
282
|
-
return self
|
|
283
|
-
|
|
284
|
-
def extend(self, value: Any) -> LogBuilder:
|
|
285
|
-
"""Add extension fields from a dataclass or mapping."""
|
|
286
|
-
if hasattr(value, "__dataclass_fields__"):
|
|
287
|
-
from dataclasses import asdict
|
|
288
|
-
mapping = asdict(value)
|
|
289
|
-
elif isinstance(value, Mapping):
|
|
290
|
-
mapping = value
|
|
291
|
-
else:
|
|
292
|
-
self._errors.append("extend() requires a dataclass or mapping")
|
|
293
|
-
return self
|
|
294
|
-
|
|
295
|
-
for k, v in mapping.items():
|
|
296
|
-
if k in ("message", "level", "code"):
|
|
297
|
-
self._errors.append(f"cannot override reserved field '{k}'")
|
|
298
|
-
else:
|
|
299
|
-
self._fields[k] = v
|
|
300
|
-
return self
|
|
301
|
-
|
|
302
219
|
def trace(self, obj: Any) -> LogBuilder:
|
|
303
220
|
"""Set trace context."""
|
|
304
221
|
if not isinstance(obj, dict):
|
|
@@ -311,11 +228,9 @@ class LogBuilder:
|
|
|
311
228
|
"""Build and return the Event, raising EventBuildError if there are errors."""
|
|
312
229
|
if self._errors:
|
|
313
230
|
raise EventBuildError("; ".join(self._errors))
|
|
314
|
-
log = {"level": self._level.value, "message": self._message}
|
|
315
|
-
log.update(self._fields)
|
|
316
231
|
envelope = {
|
|
317
232
|
"kind": "log",
|
|
318
|
-
"log":
|
|
233
|
+
"log": self._payload,
|
|
319
234
|
"trace": self._trace,
|
|
320
235
|
}
|
|
321
236
|
return Event(envelope)
|
|
@@ -331,23 +246,22 @@ def json_error(code: str, message: str) -> ErrorBuilder:
|
|
|
331
246
|
return ErrorBuilder(code, message)
|
|
332
247
|
|
|
333
248
|
|
|
334
|
-
def json_progress(
|
|
249
|
+
def json_progress(payload: Any) -> ProgressBuilder:
|
|
335
250
|
"""Create a fluent progress builder."""
|
|
336
|
-
return ProgressBuilder(
|
|
251
|
+
return ProgressBuilder(payload)
|
|
337
252
|
|
|
338
253
|
|
|
339
|
-
def json_log(
|
|
254
|
+
def json_log(payload: Any) -> LogBuilder:
|
|
340
255
|
"""Create a fluent log builder."""
|
|
341
|
-
return LogBuilder(
|
|
256
|
+
return LogBuilder(payload)
|
|
342
257
|
|
|
343
258
|
|
|
344
259
|
def validate_protocol_event(event: Any, *, strict: bool = True) -> None:
|
|
345
260
|
"""Validate one protocol v1 event envelope.
|
|
346
261
|
|
|
347
262
|
With ``strict=True`` (the default), also enforces the recommended strict
|
|
348
|
-
protocol profile (required trace
|
|
349
|
-
|
|
350
|
-
envelope-shape check only.
|
|
263
|
+
protocol profile (required trace and required error.retryable). Pass
|
|
264
|
+
``strict=False`` for the plain, lenient envelope-shape check only.
|
|
351
265
|
"""
|
|
352
266
|
if not isinstance(event, dict):
|
|
353
267
|
raise ValueError("event must be a JSON object")
|
|
@@ -370,10 +284,6 @@ def validate_protocol_event(event: Any, *, strict: bool = True) -> None:
|
|
|
370
284
|
raise ValueError("event.trace is required by the strict profile")
|
|
371
285
|
if kind == "error":
|
|
372
286
|
_validate_strict_error_payload(event["error"])
|
|
373
|
-
elif kind == "log":
|
|
374
|
-
_validate_strict_log_payload(event["log"])
|
|
375
|
-
elif kind == "progress":
|
|
376
|
-
_validate_strict_progress_payload(event["progress"])
|
|
377
287
|
|
|
378
288
|
|
|
379
289
|
def _validate_error_payload(error: Any) -> None:
|
|
@@ -426,26 +336,6 @@ def _validate_strict_error_payload(error: Any) -> None:
|
|
|
426
336
|
raise ValueError("event.error.retryable must be a boolean in the strict profile")
|
|
427
337
|
|
|
428
338
|
|
|
429
|
-
def _validate_strict_log_payload(log: Any) -> None:
|
|
430
|
-
if not isinstance(log, dict):
|
|
431
|
-
raise ValueError("event.log must be a JSON object in the strict profile")
|
|
432
|
-
if "code" in log:
|
|
433
|
-
raise ValueError("event.log must not contain 'code' field in the strict profile")
|
|
434
|
-
_require_non_empty_string(log, "message", "event.log")
|
|
435
|
-
if "level" not in log:
|
|
436
|
-
raise ValueError("event.log.level is required by the strict profile")
|
|
437
|
-
if log.get("level") not in ("debug", "info", "warn", "error"):
|
|
438
|
-
raise ValueError(
|
|
439
|
-
"event.log.level must be one of debug, info, warn, error in the strict profile"
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
def _validate_strict_progress_payload(progress: Any) -> None:
|
|
444
|
-
if not isinstance(progress, dict):
|
|
445
|
-
raise ValueError("event.progress must be a JSON object in the strict profile")
|
|
446
|
-
_require_non_empty_string(progress, "message", "event.progress")
|
|
447
|
-
|
|
448
|
-
|
|
449
339
|
def _require_non_empty_string(payload: dict, field: str, path: str) -> None:
|
|
450
340
|
value = payload.get(field)
|
|
451
341
|
if not isinstance(value, str) or value == "":
|
|
@@ -487,22 +377,17 @@ class DecodedError:
|
|
|
487
377
|
|
|
488
378
|
@dataclass(frozen=True)
|
|
489
379
|
class DecodedProgress:
|
|
490
|
-
"""Decoded protocol v1 progress event.
|
|
491
|
-
beyond message."""
|
|
380
|
+
"""Decoded protocol v1 progress event."""
|
|
492
381
|
|
|
493
|
-
|
|
494
|
-
fields: dict[str, Any] = field(default_factory=dict)
|
|
382
|
+
progress: Any
|
|
495
383
|
trace: dict | None = None
|
|
496
384
|
|
|
497
385
|
|
|
498
386
|
@dataclass(frozen=True)
|
|
499
387
|
class DecodedLog:
|
|
500
|
-
"""Decoded protocol v1 log event.
|
|
501
|
-
beyond level/message."""
|
|
388
|
+
"""Decoded protocol v1 log event."""
|
|
502
389
|
|
|
503
|
-
|
|
504
|
-
message: str
|
|
505
|
-
fields: dict[str, Any] = field(default_factory=dict)
|
|
390
|
+
log: Any
|
|
506
391
|
trace: dict | None = None
|
|
507
392
|
|
|
508
393
|
|
|
@@ -544,16 +429,10 @@ def decode_protocol_event(
|
|
|
544
429
|
)
|
|
545
430
|
|
|
546
431
|
if kind == "progress":
|
|
547
|
-
progress
|
|
548
|
-
fields = {k: v for k, v in progress.items() if k != "message"}
|
|
549
|
-
return DecodedProgress(message=progress["message"], fields=fields, trace=trace)
|
|
432
|
+
return DecodedProgress(progress=event["progress"], trace=trace)
|
|
550
433
|
|
|
551
434
|
# kind == "log"
|
|
552
|
-
log
|
|
553
|
-
fields = {k: v for k, v in log.items() if k not in ("level", "message")}
|
|
554
|
-
return DecodedLog(
|
|
555
|
-
level=LogLevel(log["level"]), message=log["message"], fields=fields, trace=trace
|
|
556
|
-
)
|
|
435
|
+
return DecodedLog(log=event["log"], trace=trace)
|
|
557
436
|
|
|
558
437
|
|
|
559
438
|
# ═══════════════════════════════════════════
|
|
@@ -678,58 +557,6 @@ def _apply_redaction_policy_with_context(
|
|
|
678
557
|
_redact_secrets(value, context)
|
|
679
558
|
|
|
680
559
|
|
|
681
|
-
def parse_size(s: str) -> int | None:
|
|
682
|
-
"""Parse a human-readable size string into bytes.
|
|
683
|
-
|
|
684
|
-
Accepts numbers followed by explicit units. Decimal units are
|
|
685
|
-
B/kB/MB/GB/TB; binary units are KiB/MiB/GiB/TiB. Trims whitespace and
|
|
686
|
-
rejects ambiguous K/M/G/T units.
|
|
687
|
-
"""
|
|
688
|
-
_multipliers = {
|
|
689
|
-
"KiB": 1024,
|
|
690
|
-
"MiB": 1024**2,
|
|
691
|
-
"GiB": 1024**3,
|
|
692
|
-
"TiB": 1024**4,
|
|
693
|
-
"kB": 1000,
|
|
694
|
-
"MB": 1000**2,
|
|
695
|
-
"GB": 1000**3,
|
|
696
|
-
"TB": 1000**4,
|
|
697
|
-
"B": 1,
|
|
698
|
-
}
|
|
699
|
-
_max_safe_integer = (1 << 53) - 1
|
|
700
|
-
s = s.strip()
|
|
701
|
-
if not s:
|
|
702
|
-
return None
|
|
703
|
-
matched = next(((unit, mult) for unit, mult in _multipliers.items() if s.endswith(unit)), None)
|
|
704
|
-
if matched is None:
|
|
705
|
-
return None
|
|
706
|
-
unit, mult = matched
|
|
707
|
-
num_str = s[: -len(unit)]
|
|
708
|
-
if not num_str:
|
|
709
|
-
return None
|
|
710
|
-
if not re.fullmatch(r"(?:\d+(?:\.\d*)?|\.\d+)(?:[eE][+-]?\d+)?", num_str):
|
|
711
|
-
return None
|
|
712
|
-
try:
|
|
713
|
-
n = int(num_str)
|
|
714
|
-
if n < 0:
|
|
715
|
-
return None
|
|
716
|
-
if n > _max_safe_integer // mult:
|
|
717
|
-
return None
|
|
718
|
-
return n * mult
|
|
719
|
-
except ValueError:
|
|
720
|
-
pass
|
|
721
|
-
try:
|
|
722
|
-
f = float(num_str)
|
|
723
|
-
if f < 0 or not math.isfinite(f):
|
|
724
|
-
return None
|
|
725
|
-
result = f * mult
|
|
726
|
-
if not math.isfinite(result) or result > _max_safe_integer:
|
|
727
|
-
return None
|
|
728
|
-
return int(result)
|
|
729
|
-
except (ValueError, OverflowError):
|
|
730
|
-
return None
|
|
731
|
-
|
|
732
|
-
|
|
733
560
|
def normalize_utc_offset(value: str) -> str | None:
|
|
734
561
|
"""Normalize a fixed UTC offset string to "UTC" or ±HH:MM.
|
|
735
562
|
|
|
@@ -784,6 +611,64 @@ def is_valid_rfc3339_time(value: str) -> bool:
|
|
|
784
611
|
return value[8] == "." and len(value) > 9 and value[9:].isdigit()
|
|
785
612
|
|
|
786
613
|
|
|
614
|
+
def is_valid_rfc3339(value: str) -> bool:
|
|
615
|
+
"""Return true when value is a complete RFC 3339 date-time.
|
|
616
|
+
|
|
617
|
+
Composed from is_valid_rfc3339_date and is_valid_rfc3339_time: a full-date, a
|
|
618
|
+
T/t separator, a partial-time (with optional fractional seconds), and a
|
|
619
|
+
mandatory time-offset (Z/z or ±HH:MM with HH in 00..23 and MM in 00..59).
|
|
620
|
+
The offset is required, so a bare 2026-02-14T10:30:00 is rejected; a space
|
|
621
|
+
separator is rejected; and a leap second (:60) is rejected, matching
|
|
622
|
+
is_valid_rfc3339_time. Non-ASCII input is rejected.
|
|
623
|
+
"""
|
|
624
|
+
if not isinstance(value, str) or len(value) < 20 or not value.isascii():
|
|
625
|
+
return False
|
|
626
|
+
if not is_valid_rfc3339_date(value[0:10]):
|
|
627
|
+
return False
|
|
628
|
+
if value[10] not in ("T", "t"):
|
|
629
|
+
return False
|
|
630
|
+
rest = value[11:]
|
|
631
|
+
if rest[-1] in ("Z", "z"):
|
|
632
|
+
partial = rest[:-1]
|
|
633
|
+
else:
|
|
634
|
+
if len(rest) < 6 or not _is_rfc3339_numoffset(rest[-6:]):
|
|
635
|
+
return False
|
|
636
|
+
partial = rest[:-6]
|
|
637
|
+
return is_valid_rfc3339_time(partial)
|
|
638
|
+
|
|
639
|
+
|
|
640
|
+
def _is_rfc3339_numoffset(offset: str) -> bool:
|
|
641
|
+
if len(offset) != 6 or offset[0] not in ("+", "-") or offset[3] != ":":
|
|
642
|
+
return False
|
|
643
|
+
hours = _parse_ascii_int(offset[1:3])
|
|
644
|
+
minutes = _parse_ascii_int(offset[4:6])
|
|
645
|
+
if hours is None or minutes is None:
|
|
646
|
+
return False
|
|
647
|
+
return hours <= 23 and minutes <= 59
|
|
648
|
+
|
|
649
|
+
|
|
650
|
+
def is_valid_bcp47(value: str) -> bool:
|
|
651
|
+
"""Return true when value is a structurally well-formed BCP 47 language tag.
|
|
652
|
+
|
|
653
|
+
A grammar-level check, not a registry lookup: hyphen-separated ASCII-alphanumeric
|
|
654
|
+
subtags (each 1-8 chars) whose primary subtag is a 2-3 letter language code or the
|
|
655
|
+
x/i privateuse/grandfathered lead. Rejects the POSIX underscore form (zh_CN), empty
|
|
656
|
+
or misplaced hyphens, non-ASCII, and out-of-range primaries such as chinese. Does
|
|
657
|
+
not verify that subtags are registered with IANA.
|
|
658
|
+
"""
|
|
659
|
+
if not isinstance(value, str) or not value:
|
|
660
|
+
return False
|
|
661
|
+
for index, subtag in enumerate(value.split("-")):
|
|
662
|
+
if not (1 <= len(subtag) <= 8) or not subtag.isascii() or not subtag.isalnum():
|
|
663
|
+
return False
|
|
664
|
+
if index == 0:
|
|
665
|
+
is_language = 2 <= len(subtag) <= 3 and subtag.isalpha()
|
|
666
|
+
is_special = subtag in ("x", "i")
|
|
667
|
+
if not is_language and not is_special:
|
|
668
|
+
return False
|
|
669
|
+
return True
|
|
670
|
+
|
|
671
|
+
|
|
787
672
|
def _parse_utc_offset_body(body: str) -> tuple[int, int] | None:
|
|
788
673
|
if not body:
|
|
789
674
|
return None
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.17.0
|
|
4
4
|
Summary: A naming convention that lets AI agents understand your data without being told what it means.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
|
|
@@ -28,7 +28,7 @@ print(output_json(value))
|
|
|
28
28
|
print(output_plain(value))
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Useful names use Python casing: `output_json`, `output_yaml`, `output_plain` (each takes an optional keyword-only `options=`), `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `
|
|
31
|
+
Useful names use Python casing: `output_json`, `output_yaml`, `output_plain` (each takes an optional keyword-only `options=`), `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `decode_protocol_event`. Skill admin and stream-redirect helpers live in `agent_first_data.skill` and `agent_first_data.stream_redirect` (import the submodule directly).
|
|
32
32
|
|
|
33
33
|
Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
|
|
34
34
|
|
|
@@ -140,7 +140,7 @@ def test_cli_output_dispatches_raw_yaml_with_options():
|
|
|
140
140
|
def test_cli_emitter_writes_events_and_tracks_terminal():
|
|
141
141
|
writer = StringIO()
|
|
142
142
|
emitter = CliEmitter(writer, OutputFormat.JSON)
|
|
143
|
-
emitter.emit(json_log(
|
|
143
|
+
emitter.emit(json_log({"level": "info", "message": "startup"}).build())
|
|
144
144
|
emitter.emit(json_result({"rows": 2}).build())
|
|
145
145
|
lines = writer.getvalue().splitlines()
|
|
146
146
|
assert len(lines) == 2
|
|
@@ -150,7 +150,7 @@ def test_cli_emitter_writes_events_and_tracks_terminal():
|
|
|
150
150
|
|
|
151
151
|
def test_cli_emitter_framing_all_formats():
|
|
152
152
|
events = [
|
|
153
|
-
json_log(
|
|
153
|
+
json_log({"level": "info", "message": "startup"}).build(),
|
|
154
154
|
json_result({"rows": 2}).build(),
|
|
155
155
|
]
|
|
156
156
|
for fmt in (OutputFormat.JSON, OutputFormat.PLAIN, OutputFormat.YAML):
|
|
@@ -238,13 +238,20 @@ def test_cli_emitter_convenience_methods():
|
|
|
238
238
|
def test_cli_emitter_with_log_fields_provider():
|
|
239
239
|
writer = StringIO()
|
|
240
240
|
def log_fields():
|
|
241
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
"source": "test",
|
|
243
|
+
"code": "cache_miss",
|
|
244
|
+
"message": "provider default",
|
|
245
|
+
"level": "debug",
|
|
246
|
+
}
|
|
242
247
|
emitter = CliEmitter(writer, OutputFormat.JSON, log_fields=log_fields)
|
|
243
248
|
emitter.emit_log(LogLevel.INFO, "test message")
|
|
244
249
|
lines = writer.getvalue().splitlines()
|
|
245
250
|
parsed = json.loads(lines[0])
|
|
246
251
|
assert parsed["log"]["source"] == "test"
|
|
252
|
+
assert parsed["log"]["code"] == "cache_miss"
|
|
247
253
|
assert parsed["log"]["message"] == "test message"
|
|
254
|
+
assert parsed["log"]["level"] == "info"
|
|
248
255
|
|
|
249
256
|
|
|
250
257
|
# ── version helpers ───────────────────────────────────────────────────────────
|
|
@@ -28,12 +28,13 @@ from agent_first_data import (
|
|
|
28
28
|
normalize_utc_offset,
|
|
29
29
|
is_valid_rfc3339_date,
|
|
30
30
|
is_valid_rfc3339_time,
|
|
31
|
+
is_valid_rfc3339,
|
|
32
|
+
is_valid_bcp47,
|
|
31
33
|
)
|
|
32
34
|
from agent_first_data.format import (
|
|
33
35
|
_format_bytes_human,
|
|
34
36
|
_format_with_commas,
|
|
35
37
|
_extract_currency_code,
|
|
36
|
-
parse_size,
|
|
37
38
|
)
|
|
38
39
|
|
|
39
40
|
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "spec", "fixtures")
|
|
@@ -107,8 +108,8 @@ def _build_protocol_case_event(typ, args):
|
|
|
107
108
|
|
|
108
109
|
args vocabulary: "result" (payload), "code"+"message" (error),
|
|
109
110
|
"hint" (-> .hint()), "retryable" (bool -> .retryable_if()),
|
|
110
|
-
"fields" (object -> .fields()), "trace" (object -> .trace()),
|
|
111
|
-
|
|
111
|
+
"fields" (object -> .fields()), "trace" (object -> .trace()), and complete
|
|
112
|
+
progress/log payloads.
|
|
112
113
|
"""
|
|
113
114
|
kind = typ.split("_", 1)[0]
|
|
114
115
|
|
|
@@ -121,13 +122,13 @@ def _build_protocol_case_event(typ, args):
|
|
|
121
122
|
if "retryable" in args:
|
|
122
123
|
builder = builder.retryable_if(args["retryable"])
|
|
123
124
|
elif kind == "progress":
|
|
124
|
-
builder = json_progress(args["message"])
|
|
125
|
+
builder = json_progress({"message": args["message"], **args.get("fields", {})})
|
|
125
126
|
elif kind == "log":
|
|
126
|
-
builder = json_log(
|
|
127
|
+
builder = json_log({"level": args["level"], "message": args["message"], **args.get("fields", {})})
|
|
127
128
|
else:
|
|
128
129
|
raise ValueError(f"unknown fixture type: {typ}")
|
|
129
130
|
|
|
130
|
-
if "fields" in args:
|
|
131
|
+
if "fields" in args and kind not in ("progress", "log"):
|
|
131
132
|
builder = builder.fields(args["fields"])
|
|
132
133
|
if "trace" in args:
|
|
133
134
|
builder = builder.trace(args["trace"])
|
|
@@ -205,9 +206,6 @@ def test_helper_fixtures():
|
|
|
205
206
|
elif name == "extract_currency_code":
|
|
206
207
|
got = _extract_currency_code(inp)
|
|
207
208
|
assert got == expected, f"[helpers/{name}({inp!r})] got {got!r}"
|
|
208
|
-
elif name == "parse_size":
|
|
209
|
-
got = parse_size(inp)
|
|
210
|
-
assert got == expected, f"[helpers/{name}({inp!r})] got {got!r}"
|
|
211
209
|
elif name == "normalize_utc_offset":
|
|
212
210
|
got = normalize_utc_offset(inp)
|
|
213
211
|
assert got == expected, f"[helpers/{name}({inp!r})] got {got!r}"
|
|
@@ -217,6 +215,12 @@ def test_helper_fixtures():
|
|
|
217
215
|
elif name == "is_valid_rfc3339_time":
|
|
218
216
|
got = is_valid_rfc3339_time(inp)
|
|
219
217
|
assert got == expected, f"[helpers/{name}({inp!r})] got {got!r}"
|
|
218
|
+
elif name == "is_valid_bcp47":
|
|
219
|
+
got = is_valid_bcp47(inp)
|
|
220
|
+
assert got == expected, f"[helpers/{name}({inp!r})] got {got!r}"
|
|
221
|
+
elif name == "is_valid_rfc3339":
|
|
222
|
+
got = is_valid_rfc3339(inp)
|
|
223
|
+
assert got == expected, f"[helpers/{name}({inp!r})] got {got!r}"
|
|
220
224
|
|
|
221
225
|
|
|
222
226
|
def test_output_format_fixtures():
|
|
@@ -388,21 +392,18 @@ def test_decode_protocol_event_error():
|
|
|
388
392
|
|
|
389
393
|
|
|
390
394
|
def test_decode_protocol_event_progress():
|
|
391
|
-
event = json_progress("halfway"
|
|
395
|
+
event = json_progress({"message": "halfway", "percent": 50}).build()
|
|
392
396
|
decoded = decode_protocol_event(output_json(event.to_dict()))
|
|
393
397
|
assert isinstance(decoded, DecodedProgress)
|
|
394
|
-
assert decoded.
|
|
395
|
-
assert decoded.fields == {"percent": 50}
|
|
398
|
+
assert decoded.progress == {"message": "halfway", "percent": 50}
|
|
396
399
|
assert decoded.trace == {}
|
|
397
400
|
|
|
398
401
|
|
|
399
402
|
def test_decode_protocol_event_log():
|
|
400
|
-
event = json_log(
|
|
403
|
+
event = json_log({"level": "warn", "message": "slow query", "duration_ms": 900}).build()
|
|
401
404
|
decoded = decode_protocol_event(output_json(event.to_dict()))
|
|
402
405
|
assert isinstance(decoded, DecodedLog)
|
|
403
|
-
assert decoded.
|
|
404
|
-
assert decoded.message == "slow query"
|
|
405
|
-
assert decoded.fields == {"duration_ms": 900}
|
|
406
|
+
assert decoded.log == {"level": "warn", "message": "slow query", "duration_ms": 900}
|
|
406
407
|
assert decoded.trace == {}
|
|
407
408
|
|
|
408
409
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_first_data-0.16.0 → agent_first_data-0.17.0}/agent_first_data.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|