agent-first-data 0.30.0__tar.gz → 0.32.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.30.0 → agent_first_data-0.32.0}/PKG-INFO +5 -2
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/README.md +4 -1
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/__init__.py +2 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/assets/cli-spec-v1.schema.json +87 -1
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/cli.py +10 -8
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/format.py +226 -30
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data.egg-info/PKG-INFO +5 -2
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/pyproject.toml +1 -1
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/tests/test_cli.py +15 -12
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/tests/test_format.py +38 -3
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/assets/cli-help-v2.schema.json +0 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/assets/protocol-v1.schema.json +0 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/assets/registry.json +0 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data.egg-info/SOURCES.txt +0 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/setup.cfg +0 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/tests/test_no_stderr_policy.py +0 -0
- {agent_first_data-0.30.0 → agent_first_data-0.32.0}/tests/test_requires_python_floor.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.32.0
|
|
4
4
|
Summary: A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
|
|
@@ -28,12 +28,15 @@ print(render(value, OutputFormat.JSON))
|
|
|
28
28
|
print(render(value, OutputFormat.PLAIN))
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `redact_argv`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `decode_protocol_event`.
|
|
31
|
+
Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `redact_urls_in_text`, `redact_argv`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `decode_protocol_event`.
|
|
32
32
|
|
|
33
33
|
## Behavior Notes
|
|
34
34
|
|
|
35
35
|
- Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
|
|
36
36
|
- `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
|
|
37
|
+
- `OutputOptions.url_names` applies URL treatment to exact legacy field names
|
|
38
|
+
and recurses through collections. `redact_urls_in_text` is explicit and scans
|
|
39
|
+
only complete scheme URLs, never arbitrary prose secrets.
|
|
37
40
|
- YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON.
|
|
38
41
|
- Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
|
|
39
42
|
- `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
|
|
@@ -19,12 +19,15 @@ print(render(value, OutputFormat.JSON))
|
|
|
19
19
|
print(render(value, OutputFormat.PLAIN))
|
|
20
20
|
```
|
|
21
21
|
|
|
22
|
-
Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `redact_argv`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `decode_protocol_event`.
|
|
22
|
+
Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `redact_urls_in_text`, `redact_argv`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `decode_protocol_event`.
|
|
23
23
|
|
|
24
24
|
## Behavior Notes
|
|
25
25
|
|
|
26
26
|
- Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
|
|
27
27
|
- `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
|
|
28
|
+
- `OutputOptions.url_names` applies URL treatment to exact legacy field names
|
|
29
|
+
and recurses through collections. `redact_urls_in_text` is explicit and scans
|
|
30
|
+
only complete scheme URLs, never arbitrary prose secrets.
|
|
28
31
|
- YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON.
|
|
29
32
|
- Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
|
|
30
33
|
- `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
|
|
@@ -26,6 +26,7 @@ from agent_first_data.format import (
|
|
|
26
26
|
redacted_value,
|
|
27
27
|
redact_argv,
|
|
28
28
|
redact_url_secrets,
|
|
29
|
+
redact_urls_in_text,
|
|
29
30
|
normalize_utc_offset,
|
|
30
31
|
is_valid_bcp47,
|
|
31
32
|
is_valid_rfc3339,
|
|
@@ -72,6 +73,7 @@ __all__ = [
|
|
|
72
73
|
"redacted_value",
|
|
73
74
|
"redact_argv",
|
|
74
75
|
"redact_url_secrets",
|
|
76
|
+
"redact_urls_in_text",
|
|
75
77
|
"normalize_utc_offset",
|
|
76
78
|
"is_valid_bcp47",
|
|
77
79
|
"is_valid_rfc3339",
|
{agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/assets/cli-spec-v1.schema.json
RENAMED
|
@@ -32,6 +32,13 @@
|
|
|
32
32
|
"about": {
|
|
33
33
|
"$ref": "#/$defs/nonEmpty"
|
|
34
34
|
},
|
|
35
|
+
"shared_arguments": {
|
|
36
|
+
"description": "Arguments every command accepts, declared once. Spliced into each command and into every combination's optional set when the registry is built, so a resolved spec behaves as if each command declared them.",
|
|
37
|
+
"type": "array",
|
|
38
|
+
"items": {
|
|
39
|
+
"$ref": "#/$defs/argument"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
35
42
|
"lifecycle_output": {
|
|
36
43
|
"$ref": "#/$defs/output"
|
|
37
44
|
},
|
|
@@ -164,7 +171,8 @@
|
|
|
164
171
|
"i64",
|
|
165
172
|
"finite_f64",
|
|
166
173
|
"enum",
|
|
167
|
-
"json"
|
|
174
|
+
"json",
|
|
175
|
+
"uuid"
|
|
168
176
|
]
|
|
169
177
|
},
|
|
170
178
|
"value_name": {
|
|
@@ -178,6 +186,15 @@
|
|
|
178
186
|
"$ref": "#/$defs/nonEmpty"
|
|
179
187
|
}
|
|
180
188
|
},
|
|
189
|
+
"range": {
|
|
190
|
+
"description": "Inclusive [minimum, maximum] bounds for an I64 argument, checked when the value is parsed so an out-of-range value is a usage error.",
|
|
191
|
+
"type": "array",
|
|
192
|
+
"items": {
|
|
193
|
+
"type": "integer"
|
|
194
|
+
},
|
|
195
|
+
"minItems": 2,
|
|
196
|
+
"maxItems": 2
|
|
197
|
+
},
|
|
181
198
|
"default": true,
|
|
182
199
|
"repeatable": {
|
|
183
200
|
"const": true
|
|
@@ -186,6 +203,10 @@
|
|
|
186
203
|
"type": "boolean",
|
|
187
204
|
"description": "This argument's value must never be echoed back in help, errors, or logs."
|
|
188
205
|
},
|
|
206
|
+
"sources": {
|
|
207
|
+
"description": "Sources this string argument accepts beside a literal value. The core validates that a value names only a listed scheme and renders env/file/file+FORMAT/stream syntax into help; reading the source is the host's, at the moment it chooses.",
|
|
208
|
+
"$ref": "#/$defs/sourceSet"
|
|
209
|
+
},
|
|
189
210
|
"about": {
|
|
190
211
|
"$ref": "#/$defs/nonEmpty"
|
|
191
212
|
}
|
|
@@ -351,6 +372,71 @@
|
|
|
351
372
|
}
|
|
352
373
|
}
|
|
353
374
|
]
|
|
375
|
+
},
|
|
376
|
+
"sourceSet": {
|
|
377
|
+
"type": "object",
|
|
378
|
+
"required": [
|
|
379
|
+
"schemes"
|
|
380
|
+
],
|
|
381
|
+
"additionalProperties": false,
|
|
382
|
+
"anyOf": [
|
|
383
|
+
{
|
|
384
|
+
"properties": {
|
|
385
|
+
"schemes": {
|
|
386
|
+
"minItems": 1
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"required": [
|
|
392
|
+
"host_schemes"
|
|
393
|
+
],
|
|
394
|
+
"properties": {
|
|
395
|
+
"host_schemes": {
|
|
396
|
+
"minItems": 1
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
],
|
|
401
|
+
"properties": {
|
|
402
|
+
"schemes": {
|
|
403
|
+
"type": "array",
|
|
404
|
+
"uniqueItems": true,
|
|
405
|
+
"items": {
|
|
406
|
+
"enum": [
|
|
407
|
+
"env",
|
|
408
|
+
"file",
|
|
409
|
+
"stdin",
|
|
410
|
+
"fd",
|
|
411
|
+
"prompt"
|
|
412
|
+
]
|
|
413
|
+
}
|
|
414
|
+
},
|
|
415
|
+
"host_schemes": {
|
|
416
|
+
"description": "Schemes the host parses and reads itself, declared so help and validation know they exist. Names must not collide with each other or the built-in/reserved grammar; CliSpec::build validates that each syntax begins with its own name and a colon.",
|
|
417
|
+
"type": "array",
|
|
418
|
+
"minItems": 1,
|
|
419
|
+
"uniqueItems": true,
|
|
420
|
+
"items": {
|
|
421
|
+
"type": "object",
|
|
422
|
+
"required": [
|
|
423
|
+
"name",
|
|
424
|
+
"syntax"
|
|
425
|
+
],
|
|
426
|
+
"additionalProperties": false,
|
|
427
|
+
"properties": {
|
|
428
|
+
"name": {
|
|
429
|
+
"type": "string",
|
|
430
|
+
"pattern": "^[a-z][a-z0-9-]*$"
|
|
431
|
+
},
|
|
432
|
+
"syntax": {
|
|
433
|
+
"$ref": "#/$defs/nonEmpty",
|
|
434
|
+
"description": "Displayed source syntax beginning with the exact host scheme name, a colon, and a non-empty value placeholder."
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
354
440
|
}
|
|
355
441
|
}
|
|
356
442
|
}
|
|
@@ -52,21 +52,22 @@ class OutputTo(enum.Enum):
|
|
|
52
52
|
def parse(cls, value: str) -> OutputTo:
|
|
53
53
|
"""Parse an ``--output-to`` value: ``split`` (default), ``stdout``, or ``stderr``.
|
|
54
54
|
|
|
55
|
-
Raises ValueError with a message suitable for build_cli_error
|
|
55
|
+
Raises ValueError with a value-safe message suitable for build_cli_error
|
|
56
|
+
on unknown values.
|
|
56
57
|
|
|
57
58
|
>>> OutputTo.parse("split")
|
|
58
59
|
<OutputTo.SPLIT: 'split'>
|
|
59
60
|
>>> OutputTo.parse("xml")
|
|
60
61
|
Traceback (most recent call last):
|
|
61
62
|
...
|
|
62
|
-
ValueError: unsupported --output-to
|
|
63
|
+
ValueError: unsupported --output-to: expected split, stdout, or stderr
|
|
63
64
|
"""
|
|
64
65
|
try:
|
|
65
66
|
return cls(value)
|
|
66
67
|
except ValueError:
|
|
67
68
|
raise ValueError(
|
|
68
|
-
|
|
69
|
-
)
|
|
69
|
+
"unsupported --output-to: expected split, stdout, or stderr"
|
|
70
|
+
) from None
|
|
70
71
|
|
|
71
72
|
|
|
72
73
|
class LogFilters:
|
|
@@ -112,21 +113,22 @@ class LogFilters:
|
|
|
112
113
|
def cli_parse_output(s: str) -> OutputFormat:
|
|
113
114
|
"""Parse the --output flag value into an OutputFormat.
|
|
114
115
|
|
|
115
|
-
Raises ValueError with a message suitable for build_cli_error on
|
|
116
|
+
Raises ValueError with a value-safe message suitable for build_cli_error on
|
|
117
|
+
unknown values.
|
|
116
118
|
|
|
117
119
|
>>> cli_parse_output("json")
|
|
118
120
|
<OutputFormat.JSON: 'json'>
|
|
119
121
|
>>> cli_parse_output("xml")
|
|
120
122
|
Traceback (most recent call last):
|
|
121
123
|
...
|
|
122
|
-
ValueError: invalid --output format
|
|
124
|
+
ValueError: invalid --output format: expected json, yaml, or plain
|
|
123
125
|
"""
|
|
124
126
|
try:
|
|
125
127
|
return OutputFormat(s)
|
|
126
128
|
except ValueError:
|
|
127
129
|
raise ValueError(
|
|
128
|
-
|
|
129
|
-
)
|
|
130
|
+
"invalid --output format: expected json, yaml, or plain"
|
|
131
|
+
) from None
|
|
130
132
|
|
|
131
133
|
|
|
132
134
|
def cli_parse_log_filters(entries: list[str]) -> LogFilters:
|
|
@@ -539,6 +539,8 @@ class OutputOptions:
|
|
|
539
539
|
# Exact field-name matches at any nesting level. The same list also matches
|
|
540
540
|
# URL query-parameter names inside _url fields (see redact_url_secrets).
|
|
541
541
|
secret_names: Sequence[str] = ()
|
|
542
|
+
# Exact legacy field names whose string leaves receive URL-aware redaction.
|
|
543
|
+
url_names: Sequence[str] = ()
|
|
542
544
|
# None or RedactionPolicy.All = full redaction (default).
|
|
543
545
|
policy: RedactionPolicy | None = None
|
|
544
546
|
style: PlainStyle = PlainStyle.Readable
|
|
@@ -555,7 +557,12 @@ def _format_json(value: Any, *, options: OutputOptions | None = None) -> str:
|
|
|
555
557
|
Single-line JSON. Secrets redacted, original keys, raw values.
|
|
556
558
|
"""
|
|
557
559
|
output_options = options or OutputOptions()
|
|
558
|
-
redacted = redacted_value(
|
|
560
|
+
redacted = redacted_value(
|
|
561
|
+
value,
|
|
562
|
+
secret_names=output_options.secret_names,
|
|
563
|
+
url_names=output_options.url_names,
|
|
564
|
+
policy=output_options.policy,
|
|
565
|
+
)
|
|
559
566
|
return _encode_json_lossless(redacted)
|
|
560
567
|
|
|
561
568
|
|
|
@@ -590,7 +597,12 @@ def _format_yaml(value: Any, *, options: OutputOptions | None = None) -> str:
|
|
|
590
597
|
types, secrets redacted. `PlainStyle` has no effect on YAML.
|
|
591
598
|
"""
|
|
592
599
|
output_options = options or OutputOptions()
|
|
593
|
-
value = redacted_value(
|
|
600
|
+
value = redacted_value(
|
|
601
|
+
value,
|
|
602
|
+
secret_names=output_options.secret_names,
|
|
603
|
+
url_names=output_options.url_names,
|
|
604
|
+
policy=output_options.policy,
|
|
605
|
+
)
|
|
594
606
|
lines = ["---"]
|
|
595
607
|
_render_yaml_raw(value, 0, lines)
|
|
596
608
|
return "\n".join(lines)
|
|
@@ -602,7 +614,12 @@ def _format_plain(value: Any, *, options: OutputOptions | None = None) -> str:
|
|
|
602
614
|
Single-line logfmt. Keys stripped, values formatted, secrets redacted.
|
|
603
615
|
"""
|
|
604
616
|
output_options = options or OutputOptions()
|
|
605
|
-
value = redacted_value(
|
|
617
|
+
value = redacted_value(
|
|
618
|
+
value,
|
|
619
|
+
secret_names=output_options.secret_names,
|
|
620
|
+
url_names=output_options.url_names,
|
|
621
|
+
policy=output_options.policy,
|
|
622
|
+
)
|
|
606
623
|
if not isinstance(value, dict):
|
|
607
624
|
return _plain_scalar(value)
|
|
608
625
|
pairs: list[tuple[str, str]] = []
|
|
@@ -624,14 +641,20 @@ def _format_plain(value: Any, *, options: OutputOptions | None = None) -> str:
|
|
|
624
641
|
# ═══════════════════════════════════════════
|
|
625
642
|
|
|
626
643
|
|
|
627
|
-
def redacted_value(
|
|
644
|
+
def redacted_value(
|
|
645
|
+
value: Any,
|
|
646
|
+
*,
|
|
647
|
+
secret_names: Sequence[str] = (),
|
|
648
|
+
url_names: Sequence[str] = (),
|
|
649
|
+
policy: RedactionPolicy | None = None,
|
|
650
|
+
) -> Any:
|
|
628
651
|
"""Return a JSON-safe copy with redaction options applied.
|
|
629
652
|
|
|
630
653
|
Redacts fields ending in _secret/_SECRET and those listed in secret_names.
|
|
631
|
-
Redacts _url fields
|
|
654
|
+
Redacts _url fields and exact ``url_names`` entries by the same rules.
|
|
632
655
|
"""
|
|
633
656
|
v = _sanitize_for_json(value)
|
|
634
|
-
_apply_redaction(v, secret_names, policy)
|
|
657
|
+
_apply_redaction(v, secret_names, url_names, policy)
|
|
635
658
|
return v
|
|
636
659
|
|
|
637
660
|
|
|
@@ -665,6 +688,23 @@ def redact_url_secrets(
|
|
|
665
688
|
return redacted if redacted is not None else url
|
|
666
689
|
|
|
667
690
|
|
|
691
|
+
def redact_urls_in_text(
|
|
692
|
+
text: str,
|
|
693
|
+
*,
|
|
694
|
+
secret_names: Sequence[str] = (),
|
|
695
|
+
policy: RedactionPolicy | None = None,
|
|
696
|
+
) -> str:
|
|
697
|
+
"""Redact complete scheme-prefixed URL spans embedded in prose.
|
|
698
|
+
|
|
699
|
+
This helper is explicit and URL-only: surrounding text is never scanned
|
|
700
|
+
for secret-looking names or values.
|
|
701
|
+
"""
|
|
702
|
+
if not _redacts_unscoped_input(policy):
|
|
703
|
+
return text
|
|
704
|
+
context = _RedactionContext.from_names(secret_names)
|
|
705
|
+
return _redact_urls_in_text_with_context(text, context)
|
|
706
|
+
|
|
707
|
+
|
|
668
708
|
def redact_argv(
|
|
669
709
|
args: Sequence[str],
|
|
670
710
|
*,
|
|
@@ -725,8 +765,13 @@ def _is_secret_flag_name(flag_name: str, context: _RedactionContext) -> bool:
|
|
|
725
765
|
return context.is_secret_key(normalized) or context.is_secret_key(flag_name)
|
|
726
766
|
|
|
727
767
|
|
|
728
|
-
def _apply_redaction(
|
|
729
|
-
|
|
768
|
+
def _apply_redaction(
|
|
769
|
+
value: Any,
|
|
770
|
+
secret_names: Sequence[str],
|
|
771
|
+
url_names: Sequence[str],
|
|
772
|
+
policy: RedactionPolicy | None,
|
|
773
|
+
) -> None:
|
|
774
|
+
context = _RedactionContext.from_names(secret_names, url_names)
|
|
730
775
|
_apply_redaction_policy_with_context(value, policy, context)
|
|
731
776
|
|
|
732
777
|
|
|
@@ -987,17 +1032,28 @@ def _sanitize_for_json(value: Any, stack: set[int] | None = None, depth: int = 0
|
|
|
987
1032
|
|
|
988
1033
|
@dataclass(frozen=True)
|
|
989
1034
|
class _RedactionContext:
|
|
990
|
-
"""Internal redaction context:
|
|
1035
|
+
"""Internal redaction context: exact secret-name and URL-name sets."""
|
|
991
1036
|
|
|
992
1037
|
secret_names: frozenset[str] = frozenset()
|
|
1038
|
+
url_names: frozenset[str] = frozenset()
|
|
993
1039
|
|
|
994
1040
|
@classmethod
|
|
995
|
-
def from_names(
|
|
996
|
-
|
|
1041
|
+
def from_names(
|
|
1042
|
+
cls,
|
|
1043
|
+
secret_names: Sequence[str],
|
|
1044
|
+
url_names: Sequence[str] = (),
|
|
1045
|
+
) -> _RedactionContext:
|
|
1046
|
+
return cls(
|
|
1047
|
+
secret_names=frozenset(secret_names),
|
|
1048
|
+
url_names=frozenset(url_names),
|
|
1049
|
+
)
|
|
997
1050
|
|
|
998
1051
|
def is_secret_key(self, key: str) -> bool:
|
|
999
1052
|
return _key_has_secret_suffix(key) or key in self.secret_names
|
|
1000
1053
|
|
|
1054
|
+
def is_url_key(self, key: str) -> bool:
|
|
1055
|
+
return _key_has_url_suffix(key) or key in self.url_names
|
|
1056
|
+
|
|
1001
1057
|
|
|
1002
1058
|
def _key_has_secret_suffix(key: str) -> bool:
|
|
1003
1059
|
return key.endswith("_secret") or key.endswith("_SECRET")
|
|
@@ -1021,13 +1077,8 @@ def _redact_secrets(value: Any, context: _RedactionContext = _RedactionContext()
|
|
|
1021
1077
|
# it does not invent one.
|
|
1022
1078
|
if v is not None:
|
|
1023
1079
|
value[k] = REDACTED_MARKER
|
|
1024
|
-
elif
|
|
1025
|
-
|
|
1026
|
-
value[k] = _redact_url_field_value(v, context)
|
|
1027
|
-
elif depth + 1 >= MAX_DEPTH:
|
|
1028
|
-
value[k] = MAX_DEPTH_MARKER
|
|
1029
|
-
else:
|
|
1030
|
-
_redact_secrets(v, context, depth + 1)
|
|
1080
|
+
elif context.is_url_key(k):
|
|
1081
|
+
value[k] = _redact_url_value(v, context, depth + 1)
|
|
1031
1082
|
elif depth + 1 >= MAX_DEPTH:
|
|
1032
1083
|
value[k] = MAX_DEPTH_MARKER
|
|
1033
1084
|
else:
|
|
@@ -1040,6 +1091,27 @@ def _redact_secrets(value: Any, context: _RedactionContext = _RedactionContext()
|
|
|
1040
1091
|
_redact_secrets(item, context, depth + 1)
|
|
1041
1092
|
|
|
1042
1093
|
|
|
1094
|
+
def _redact_url_value(value: Any, context: _RedactionContext, depth: int) -> Any:
|
|
1095
|
+
if depth >= MAX_DEPTH:
|
|
1096
|
+
return MAX_DEPTH_MARKER
|
|
1097
|
+
if isinstance(value, str):
|
|
1098
|
+
return _redact_url_field_value(value, context)
|
|
1099
|
+
if isinstance(value, list):
|
|
1100
|
+
for index, item in enumerate(value):
|
|
1101
|
+
value[index] = _redact_url_value(item, context, depth + 1)
|
|
1102
|
+
return value
|
|
1103
|
+
if isinstance(value, dict):
|
|
1104
|
+
for key in list(value.keys()):
|
|
1105
|
+
item = value[key]
|
|
1106
|
+
if context.is_secret_key(key):
|
|
1107
|
+
if item is not None:
|
|
1108
|
+
value[key] = REDACTED_MARKER
|
|
1109
|
+
else:
|
|
1110
|
+
value[key] = _redact_url_value(item, context, depth + 1)
|
|
1111
|
+
return value
|
|
1112
|
+
return value
|
|
1113
|
+
|
|
1114
|
+
|
|
1043
1115
|
# ═══════════════════════════════════════════
|
|
1044
1116
|
# URL-aware Secret Redaction
|
|
1045
1117
|
# ═══════════════════════════════════════════
|
|
@@ -1088,6 +1160,115 @@ def _redact_url_in_str(s: str, context: _RedactionContext) -> str | None:
|
|
|
1088
1160
|
return f"{scheme}://{new_authority}{new_before_fragment}{new_fragment}"
|
|
1089
1161
|
|
|
1090
1162
|
|
|
1163
|
+
def _redact_urls_in_text_with_context(text: str, context: _RedactionContext) -> str:
|
|
1164
|
+
output: list[str] = []
|
|
1165
|
+
copied_through = 0
|
|
1166
|
+
search_from = 0
|
|
1167
|
+
while True:
|
|
1168
|
+
span = _next_scheme_url_span(text, search_from)
|
|
1169
|
+
if span is None:
|
|
1170
|
+
break
|
|
1171
|
+
start, end = span
|
|
1172
|
+
output.append(text[copied_through:start])
|
|
1173
|
+
url = text[start:end]
|
|
1174
|
+
output.append(_redact_url_in_str(url, context) or url)
|
|
1175
|
+
copied_through = end
|
|
1176
|
+
search_from = end
|
|
1177
|
+
if not output:
|
|
1178
|
+
return text
|
|
1179
|
+
output.append(text[copied_through:])
|
|
1180
|
+
return "".join(output)
|
|
1181
|
+
|
|
1182
|
+
|
|
1183
|
+
def _next_scheme_url_span(text: str, start_at: int) -> tuple[int, int] | None:
|
|
1184
|
+
start = start_at
|
|
1185
|
+
while start < len(text):
|
|
1186
|
+
# Only consider the head of a maximal scheme-character run: a scheme
|
|
1187
|
+
# cannot begin mid-run, so a position whose predecessor is a scheme
|
|
1188
|
+
# character was already covered by the run that contains it.
|
|
1189
|
+
if start == 0 or not _is_scheme_character(text[start - 1]):
|
|
1190
|
+
scheme_end = start
|
|
1191
|
+
while scheme_end < len(text) and _is_scheme_character(text[scheme_end]):
|
|
1192
|
+
scheme_end += 1
|
|
1193
|
+
# A scheme must start with a letter, but the run need not: prose like
|
|
1194
|
+
# "2https://h/?token_secret=x" glues a digit onto the URL. Retry from
|
|
1195
|
+
# the first letter *inside* the run instead of abandoning the span --
|
|
1196
|
+
# giving up there fails open and leaks the whole query.
|
|
1197
|
+
scheme_start = next(
|
|
1198
|
+
(
|
|
1199
|
+
i
|
|
1200
|
+
for i in range(start, scheme_end)
|
|
1201
|
+
if text[i].isascii() and text[i].isalpha()
|
|
1202
|
+
),
|
|
1203
|
+
None,
|
|
1204
|
+
)
|
|
1205
|
+
if scheme_start is not None and text.startswith("://", scheme_end):
|
|
1206
|
+
end = scheme_end + 3
|
|
1207
|
+
while (
|
|
1208
|
+
end < len(text)
|
|
1209
|
+
and not _is_span_terminator_whitespace(text[end])
|
|
1210
|
+
and not _is_text_url_delimiter(text[end])
|
|
1211
|
+
):
|
|
1212
|
+
end += 1
|
|
1213
|
+
while (
|
|
1214
|
+
end > scheme_end + 3
|
|
1215
|
+
and _is_trailing_text_url_punctuation(text[end - 1])
|
|
1216
|
+
):
|
|
1217
|
+
end -= 1
|
|
1218
|
+
if end > scheme_end + 3:
|
|
1219
|
+
return scheme_start, end
|
|
1220
|
+
start += 1
|
|
1221
|
+
return None
|
|
1222
|
+
|
|
1223
|
+
|
|
1224
|
+
def _is_scheme_character(character: str) -> bool:
|
|
1225
|
+
return character.isascii() and (character.isalnum() or character in "+-.")
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
# Characters that end a URL span in prose: the Unicode White_Space property,
|
|
1229
|
+
# written out here instead of delegated to ``str.isspace()``.
|
|
1230
|
+
#
|
|
1231
|
+
# The four AFDATA implementations must cut the span at the same byte, and each
|
|
1232
|
+
# language's native predicate covers a different set -- ``str.isspace()`` counts
|
|
1233
|
+
# U+001C-U+001F, JS ``\s`` counts U+FEFF but not U+0085, Rust and Go count
|
|
1234
|
+
# neither. Divergence is a defect in both directions: ending a span early leaves
|
|
1235
|
+
# the rest of the query readable, and ending it late pulls the following prose
|
|
1236
|
+
# into the URL, where redacting the parameter it lands in deletes it. So the set
|
|
1237
|
+
# is enumerated, and it is generous -- a redaction helper exists to keep a log
|
|
1238
|
+
# line readable, so anything a human reads as a space must end the URL.
|
|
1239
|
+
#
|
|
1240
|
+
# Two exclusions are deliberate, not oversights:
|
|
1241
|
+
# * U+FEFF (zero-width no-break space) is not White_Space. Treating it as one
|
|
1242
|
+
# would cut a URL carrying it short and leave the secret after it in the
|
|
1243
|
+
# clear -- the exact leak JS ``\s`` used to cause here.
|
|
1244
|
+
# * U+001C-U+001F (the file/group/record/unit separators) are not White_Space
|
|
1245
|
+
# either; only ``str.isspace()`` counted them.
|
|
1246
|
+
#
|
|
1247
|
+
# This is a different question from :func:`_is_single_url`, which asks whether a
|
|
1248
|
+
# whole string is one URL and stays ASCII-only. A span this scanner produces
|
|
1249
|
+
# contains no whitespace at all, so it satisfies that stricter gate either way.
|
|
1250
|
+
_SPAN_TERMINATOR_WHITESPACE = frozenset(
|
|
1251
|
+
"\u0009\u000a\u000b\u000c\u000d\u0020" # TAB, LF, VT, FF, CR, SPACE
|
|
1252
|
+
"\u0085\u00a0\u1680" # NEL, NBSP, OGHAM SPACE MARK
|
|
1253
|
+
"\u2000\u2001\u2002\u2003\u2004\u2005" # EN QUAD .. FOUR-PER-EM SPACE
|
|
1254
|
+
"\u2006\u2007\u2008\u2009\u200a" # SIX-PER-EM SPACE .. HAIR SPACE
|
|
1255
|
+
"\u2028\u2029" # LINE SEPARATOR, PARAGRAPH SEPARATOR
|
|
1256
|
+
"\u202f\u205f\u3000" # NARROW NBSP, MEDIUM MATH SPACE, IDEOGRAPHIC SPACE
|
|
1257
|
+
)
|
|
1258
|
+
|
|
1259
|
+
|
|
1260
|
+
def _is_span_terminator_whitespace(character: str) -> bool:
|
|
1261
|
+
return character in _SPAN_TERMINATOR_WHITESPACE
|
|
1262
|
+
|
|
1263
|
+
|
|
1264
|
+
def _is_text_url_delimiter(character: str) -> bool:
|
|
1265
|
+
return character in "\"'<>`,。;!?)》】」』"
|
|
1266
|
+
|
|
1267
|
+
|
|
1268
|
+
def _is_trailing_text_url_punctuation(character: str) -> bool:
|
|
1269
|
+
return character in ".,;!)]},。;!)》】"
|
|
1270
|
+
|
|
1271
|
+
|
|
1091
1272
|
def _redact_url_field_value(s: str, context: _RedactionContext) -> str:
|
|
1092
1273
|
redacted = _redact_url_in_str(s, context)
|
|
1093
1274
|
if redacted is not None:
|
|
@@ -1249,18 +1430,22 @@ def _as_non_neg_int(value: Any) -> int | None:
|
|
|
1249
1430
|
return None
|
|
1250
1431
|
|
|
1251
1432
|
|
|
1252
|
-
def
|
|
1433
|
+
def _as_i64(value: Any) -> int | None:
|
|
1253
1434
|
if isinstance(value, _RawNumber):
|
|
1254
1435
|
if not re.fullmatch(r"-?\d+", value.literal):
|
|
1255
1436
|
return None
|
|
1256
1437
|
n = int(value.literal)
|
|
1257
1438
|
else:
|
|
1258
1439
|
n = _as_int(value)
|
|
1259
|
-
if n is not None and
|
|
1440
|
+
if n is not None and -9_223_372_036_854_775_808 <= n <= 9_223_372_036_854_775_807:
|
|
1260
1441
|
return n
|
|
1261
1442
|
return None
|
|
1262
1443
|
|
|
1263
1444
|
|
|
1445
|
+
def _signed_magnitude(value: int) -> tuple[str, int]:
|
|
1446
|
+
return ("-", -value) if value < 0 else ("", value)
|
|
1447
|
+
|
|
1448
|
+
|
|
1264
1449
|
def _as_decimal_int(value: Any) -> int | None:
|
|
1265
1450
|
if isinstance(value, str) and re.fullmatch(r"-?\d+", value):
|
|
1266
1451
|
return int(value)
|
|
@@ -1318,29 +1503,39 @@ def _try_process_field(key: str, value: Any) -> tuple[str, str] | None:
|
|
|
1318
1503
|
# Group 2: compound currency suffixes
|
|
1319
1504
|
stripped = _strip_suffix_ci(key, "_usd_cents")
|
|
1320
1505
|
if stripped is not None:
|
|
1321
|
-
n =
|
|
1506
|
+
n = _as_i64(value)
|
|
1322
1507
|
if n is not None:
|
|
1323
|
-
|
|
1508
|
+
sign, magnitude = _signed_magnitude(n)
|
|
1509
|
+
return stripped, f"{sign}${magnitude // 100}.{magnitude % 100:02d}"
|
|
1324
1510
|
return None
|
|
1325
1511
|
stripped = _strip_suffix_ci(key, "_eur_cents")
|
|
1326
1512
|
if stripped is not None:
|
|
1327
|
-
n =
|
|
1513
|
+
n = _as_i64(value)
|
|
1328
1514
|
if n is not None:
|
|
1329
|
-
|
|
1515
|
+
sign, magnitude = _signed_magnitude(n)
|
|
1516
|
+
return stripped, f"{sign}\u20ac{magnitude // 100}.{magnitude % 100:02d}"
|
|
1330
1517
|
return None
|
|
1331
1518
|
gc = _try_strip_generic_cents(key)
|
|
1332
1519
|
if gc is not None:
|
|
1333
1520
|
stripped, code = gc
|
|
1334
|
-
n =
|
|
1521
|
+
n = _as_i64(value)
|
|
1335
1522
|
if n is not None:
|
|
1336
|
-
|
|
1523
|
+
sign, magnitude = _signed_magnitude(n)
|
|
1524
|
+
return (
|
|
1525
|
+
stripped,
|
|
1526
|
+
f"{sign}{magnitude // 100}.{magnitude % 100:02d} {code.upper()}",
|
|
1527
|
+
)
|
|
1337
1528
|
return None
|
|
1338
1529
|
gm = _try_strip_generic_micro(key)
|
|
1339
1530
|
if gm is not None:
|
|
1340
1531
|
stripped, code = gm
|
|
1341
|
-
n =
|
|
1532
|
+
n = _as_i64(value)
|
|
1342
1533
|
if n is not None:
|
|
1343
|
-
|
|
1534
|
+
sign, magnitude = _signed_magnitude(n)
|
|
1535
|
+
return (
|
|
1536
|
+
stripped,
|
|
1537
|
+
f"{sign}{magnitude // 1_000_000}.{magnitude % 1_000_000:06d} {code.upper()}",
|
|
1538
|
+
)
|
|
1344
1539
|
return None
|
|
1345
1540
|
|
|
1346
1541
|
# Group 3: multi-char suffixes
|
|
@@ -1392,9 +1587,10 @@ def _try_process_field(key: str, value: Any) -> tuple[str, str] | None:
|
|
|
1392
1587
|
# Group 5: short suffixes (last to avoid false positives)
|
|
1393
1588
|
stripped = _strip_suffix_ci(key, "_jpy")
|
|
1394
1589
|
if stripped is not None:
|
|
1395
|
-
n =
|
|
1590
|
+
n = _as_i64(value)
|
|
1396
1591
|
if n is not None:
|
|
1397
|
-
|
|
1592
|
+
sign, magnitude = _signed_magnitude(n)
|
|
1593
|
+
return stripped, f"{sign}\u00a5{_format_with_commas(magnitude)}"
|
|
1398
1594
|
return None
|
|
1399
1595
|
stripped = _strip_suffix_ci(key, "_ns")
|
|
1400
1596
|
if stripped is not None:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agent-first-data
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.32.0
|
|
4
4
|
Summary: A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
|
|
@@ -28,12 +28,15 @@ print(render(value, OutputFormat.JSON))
|
|
|
28
28
|
print(render(value, OutputFormat.PLAIN))
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
-
Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `redact_argv`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `decode_protocol_event`.
|
|
31
|
+
Useful names use Python casing: `render` (the single value x format x options -> str entry point; takes an `OutputFormat` and an optional keyword-only `options=`), `redacted_value`, `redact_url_secrets`, `redact_urls_in_text`, `redact_argv`, `normalize_utc_offset`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `is_valid_rfc3339`, `is_valid_bcp47`, `cli_parse_output`, `build_cli_error`, `build_cli_version`, `decode_protocol_event`.
|
|
32
32
|
|
|
33
33
|
## Behavior Notes
|
|
34
34
|
|
|
35
35
|
- Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
|
|
36
36
|
- `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
|
|
37
|
+
- `OutputOptions.url_names` applies URL treatment to exact legacy field names
|
|
38
|
+
and recurses through collections. `redact_urls_in_text` is explicit and scans
|
|
39
|
+
only complete scheme URLs, never arbitrary prose secrets.
|
|
37
40
|
- YAML keeps original keys and values (structure-preserving, like JSON), sorting keys by UTF-16 code unit order and quoting/escaping unsafe keys and string scalars. Plain strips formatting suffixes, formats values, sorts the same way, and renders nested objects/arrays as canonical JSON.
|
|
38
41
|
- Logging records use `kind:"log"` with a nested `log` payload and a separate `level` field, so error-level logs are not terminal protocol errors.
|
|
39
42
|
- `build_cli_error(message, hint?)` returns a strict-ready CLI error with `error.retryable:false` and `trace:{}`.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "agent-first-data"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.32.0"
|
|
4
4
|
description = "A naming convention that lets AI agents understand your data without being told what it means, plus a CLI and library for reading Markdown structure and safely editing structured JSON, TOML, YAML, dotenv, and INI documents."
|
|
5
5
|
license = "MIT"
|
|
6
6
|
readme = "README.md"
|
|
@@ -39,11 +39,13 @@ def test_parse_output_rejects_unknown():
|
|
|
39
39
|
cli_parse_output("")
|
|
40
40
|
|
|
41
41
|
|
|
42
|
-
def
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
42
|
+
def test_parse_output_error_omits_value():
|
|
43
|
+
canary = "canary-output-secret"
|
|
44
|
+
with pytest.raises(ValueError) as raised:
|
|
45
|
+
cli_parse_output(canary)
|
|
46
|
+
assert canary not in str(raised.value)
|
|
47
|
+
assert "json" in str(raised.value)
|
|
48
|
+
assert raised.value.__suppress_context__ is True
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
# ── cli_parse_log_filters ─────────────────────────────────────────────────────
|
|
@@ -267,13 +269,14 @@ def test_output_to_parse_all_variants():
|
|
|
267
269
|
|
|
268
270
|
|
|
269
271
|
def test_output_to_parse_rejects_unknown():
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
272
|
+
canary = "canary-output-to-secret"
|
|
273
|
+
with pytest.raises(ValueError) as raised:
|
|
274
|
+
OutputTo.parse(canary)
|
|
275
|
+
message = str(raised.value)
|
|
276
|
+
assert "unsupported --output-to" in message
|
|
277
|
+
assert canary not in message
|
|
278
|
+
assert "split, stdout, or stderr" in message
|
|
279
|
+
assert raised.value.__suppress_context__ is True
|
|
277
280
|
|
|
278
281
|
|
|
279
282
|
def test_output_to_parse_is_case_sensitive():
|
|
@@ -27,6 +27,7 @@ from agent_first_data import (
|
|
|
27
27
|
redacted_value,
|
|
28
28
|
redact_argv,
|
|
29
29
|
redact_url_secrets,
|
|
30
|
+
redact_urls_in_text,
|
|
30
31
|
render,
|
|
31
32
|
normalize_utc_offset,
|
|
32
33
|
is_valid_rfc3339_date,
|
|
@@ -85,7 +86,11 @@ def test_builder_contract_fixtures():
|
|
|
85
86
|
def _redaction_options(case):
|
|
86
87
|
opts = case.get("options", {})
|
|
87
88
|
policy = RedactionPolicy(opts["policy"]) if "policy" in opts else None
|
|
88
|
-
return OutputOptions(
|
|
89
|
+
return OutputOptions(
|
|
90
|
+
policy=policy,
|
|
91
|
+
secret_names=opts.get("secret_names", ()),
|
|
92
|
+
url_names=opts.get("url_names", ()),
|
|
93
|
+
)
|
|
89
94
|
|
|
90
95
|
|
|
91
96
|
# --- Redact fixtures ---
|
|
@@ -110,6 +115,18 @@ def test_redact_url_fixtures():
|
|
|
110
115
|
assert got == case["expected"], f"[redact_url/{name}] got {got!r}"
|
|
111
116
|
|
|
112
117
|
|
|
118
|
+
def test_redact_urls_in_text_fixtures():
|
|
119
|
+
for case in _load("redact_urls_in_text.json"):
|
|
120
|
+
name = case["name"]
|
|
121
|
+
options = _redaction_options(case)
|
|
122
|
+
got = redact_urls_in_text(
|
|
123
|
+
case["input"],
|
|
124
|
+
secret_names=options.secret_names,
|
|
125
|
+
policy=options.policy,
|
|
126
|
+
)
|
|
127
|
+
assert got == case["expected"], f"[redact_urls_in_text/{name}] got {got!r}"
|
|
128
|
+
|
|
129
|
+
|
|
113
130
|
def test_redact_argv_fixtures():
|
|
114
131
|
for case in _load("redact_argv.json"):
|
|
115
132
|
name = case["name"]
|
|
@@ -133,7 +150,12 @@ def test_redaction_options_fixtures():
|
|
|
133
150
|
output_options = _redaction_options(case)
|
|
134
151
|
expected = case["expected"]
|
|
135
152
|
|
|
136
|
-
got = redacted_value(
|
|
153
|
+
got = redacted_value(
|
|
154
|
+
case["input"],
|
|
155
|
+
secret_names=output_options.secret_names,
|
|
156
|
+
url_names=output_options.url_names,
|
|
157
|
+
policy=output_options.policy,
|
|
158
|
+
)
|
|
137
159
|
assert got == expected, f"[redaction_options/{name}] value mismatch: {got}"
|
|
138
160
|
|
|
139
161
|
got_json = json.loads(render(case["input"], OutputFormat.JSON, options=output_options))
|
|
@@ -152,7 +174,12 @@ def test_security_fixtures():
|
|
|
152
174
|
for case in fixture["redaction_cases"]:
|
|
153
175
|
name = case["name"]
|
|
154
176
|
output_options = _redaction_options(case)
|
|
155
|
-
assert redacted_value(
|
|
177
|
+
assert redacted_value(
|
|
178
|
+
case["input"],
|
|
179
|
+
secret_names=output_options.secret_names,
|
|
180
|
+
url_names=output_options.url_names,
|
|
181
|
+
policy=output_options.policy,
|
|
182
|
+
) == case["expected"]
|
|
156
183
|
outputs = (
|
|
157
184
|
render(case["input"], OutputFormat.JSON, options=output_options),
|
|
158
185
|
render(case["input"], OutputFormat.YAML, options=output_options),
|
|
@@ -772,3 +799,11 @@ def test_number_fidelity_does_not_regress_ordinary_decoded_numbers_in_plain_outp
|
|
|
772
799
|
decoded = decode_protocol_event(line)
|
|
773
800
|
plain = render(decoded.result, OutputFormat.PLAIN)
|
|
774
801
|
assert plain == "cpu=85.5% duration=42ms size=5.0MiB"
|
|
802
|
+
|
|
803
|
+
|
|
804
|
+
def test_signed_fiat_i64_min_formats_without_overflow():
|
|
805
|
+
plain = render(
|
|
806
|
+
{"adjustment_usd_cents": -9_223_372_036_854_775_808},
|
|
807
|
+
OutputFormat.PLAIN,
|
|
808
|
+
)
|
|
809
|
+
assert plain == "adjustment=-$92233720368547758.08"
|
{agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/assets/cli-help-v2.schema.json
RENAMED
|
File without changes
|
{agent_first_data-0.30.0 → agent_first_data-0.32.0}/agent_first_data/assets/protocol-v1.schema.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_first_data-0.30.0 → agent_first_data-0.32.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
|