agent-first-data 0.3.0__tar.gz → 0.4.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/cmnspore/agent-first-data
@@ -74,7 +74,7 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
74
74
 
75
75
  ## API Reference
76
76
 
77
- Total: **8 public APIs** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility)
77
+ Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
78
78
 
79
79
  ### Protocol Builders (returns dict)
80
80
 
@@ -187,6 +187,41 @@ assert parse_size("1.5K") == 1536
187
187
  assert parse_size("512") == 512
188
188
  ```
189
189
 
190
+ ### CLI Helpers (for tools built on AFDATA)
191
+
192
+ Shared helpers that prevent flag-parsing drift between CLI tools. Use these instead of reimplementing `--output` and `--log` handling in each tool.
193
+
194
+ ```python
195
+ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
196
+
197
+ cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
198
+ cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
199
+ cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
200
+ build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
201
+ ```
202
+
203
+ **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
204
+
205
+ ```python
206
+ import sys
207
+ from agent_first_data import (
208
+ OutputFormat, cli_parse_output, cli_parse_log_filters,
209
+ cli_output, build_cli_error, output_json,
210
+ )
211
+
212
+ try:
213
+ fmt = cli_parse_output(args.output)
214
+ except ValueError as e:
215
+ print(output_json(build_cli_error(str(e))))
216
+ sys.exit(2)
217
+
218
+ log = cli_parse_log_filters(args.log.split(",") if args.log else [])
219
+ # ... do work ...
220
+ print(cli_output(result, fmt))
221
+ ```
222
+
223
+ See `examples/agent_cli.py` for the complete working example (`pytest examples/agent_cli.py`).
224
+
190
225
  ## Usage Examples
191
226
 
192
227
  ### Example 1: REST API
@@ -276,6 +311,7 @@ result = build_json_ok(
276
311
  )
277
312
 
278
313
  # Print JSONL to stdout (secrets redacted, one JSON object per line)
314
+ # Channel policy: machine-readable protocol/log events must not use stderr.
279
315
  print(output_json(result))
280
316
  # {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
281
317
  ```
@@ -65,7 +65,7 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
65
65
 
66
66
  ## API Reference
67
67
 
68
- Total: **8 public APIs** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility)
68
+ Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
69
69
 
70
70
  ### Protocol Builders (returns dict)
71
71
 
@@ -178,6 +178,41 @@ assert parse_size("1.5K") == 1536
178
178
  assert parse_size("512") == 512
179
179
  ```
180
180
 
181
+ ### CLI Helpers (for tools built on AFDATA)
182
+
183
+ Shared helpers that prevent flag-parsing drift between CLI tools. Use these instead of reimplementing `--output` and `--log` handling in each tool.
184
+
185
+ ```python
186
+ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
187
+
188
+ cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
189
+ cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
190
+ cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
191
+ build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
192
+ ```
193
+
194
+ **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
195
+
196
+ ```python
197
+ import sys
198
+ from agent_first_data import (
199
+ OutputFormat, cli_parse_output, cli_parse_log_filters,
200
+ cli_output, build_cli_error, output_json,
201
+ )
202
+
203
+ try:
204
+ fmt = cli_parse_output(args.output)
205
+ except ValueError as e:
206
+ print(output_json(build_cli_error(str(e))))
207
+ sys.exit(2)
208
+
209
+ log = cli_parse_log_filters(args.log.split(",") if args.log else [])
210
+ # ... do work ...
211
+ print(cli_output(result, fmt))
212
+ ```
213
+
214
+ See `examples/agent_cli.py` for the complete working example (`pytest examples/agent_cli.py`).
215
+
181
216
  ## Usage Examples
182
217
 
183
218
  ### Example 1: REST API
@@ -267,6 +302,7 @@ result = build_json_ok(
267
302
  )
268
303
 
269
304
  # Print JSONL to stdout (secrets redacted, one JSON object per line)
305
+ # Channel policy: machine-readable protocol/log events must not use stderr.
270
306
  print(output_json(result))
271
307
  # {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
272
308
  ```
@@ -21,6 +21,14 @@ from agent_first_data.afdata_logging import (
21
21
  span,
22
22
  )
23
23
 
24
+ from agent_first_data.cli import (
25
+ OutputFormat,
26
+ cli_parse_output,
27
+ cli_parse_log_filters,
28
+ cli_output,
29
+ build_cli_error,
30
+ )
31
+
24
32
  __all__ = [
25
33
  "build_json_ok",
26
34
  "build_json_error",
@@ -37,4 +45,9 @@ __all__ = [
37
45
  "init_logging_yaml",
38
46
  "get_logger",
39
47
  "span",
48
+ "OutputFormat",
49
+ "cli_parse_output",
50
+ "cli_parse_log_filters",
51
+ "cli_output",
52
+ "build_cli_error",
40
53
  ]
@@ -0,0 +1,92 @@
1
+ """AFDATA CLI helpers — output format parsing, log filter normalization, error building."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import enum
6
+ from typing import Any
7
+
8
+ from agent_first_data.format import output_json, output_yaml, output_plain
9
+
10
+
11
+ class OutputFormat(enum.Enum):
12
+ """Output format for CLI and pipe/MCP modes."""
13
+
14
+ JSON = "json"
15
+ YAML = "yaml"
16
+ PLAIN = "plain"
17
+
18
+
19
+ def cli_parse_output(s: str) -> OutputFormat:
20
+ """Parse the --output flag value into an OutputFormat.
21
+
22
+ Raises ValueError with a message suitable for build_cli_error on unknown values.
23
+
24
+ >>> cli_parse_output("json")
25
+ <OutputFormat.JSON: 'json'>
26
+ >>> cli_parse_output("xml")
27
+ Traceback (most recent call last):
28
+ ...
29
+ ValueError: invalid --output format 'xml': expected json, yaml, or plain
30
+ """
31
+ try:
32
+ return OutputFormat(s)
33
+ except ValueError:
34
+ raise ValueError(
35
+ f"invalid --output format {s!r}: expected json, yaml, or plain"
36
+ )
37
+
38
+
39
+ def cli_parse_log_filters(entries: list[str]) -> list[str]:
40
+ """Normalize --log flag entries: trim, lowercase, deduplicate, remove empty.
41
+
42
+ Accepts pre-split entries (e.g. after splitting on comma).
43
+
44
+ >>> cli_parse_log_filters(["Query", " error ", "query"])
45
+ ['query', 'error']
46
+ """
47
+ out: list[str] = []
48
+ for entry in entries:
49
+ s = entry.strip().lower()
50
+ if s and s not in out:
51
+ out.append(s)
52
+ return out
53
+
54
+
55
+ def cli_output(value: Any, format: OutputFormat) -> str:
56
+ """Dispatch output formatting by OutputFormat.
57
+
58
+ Equivalent to calling output_json, output_yaml, or output_plain directly.
59
+
60
+ >>> import json
61
+ >>> v = {"code": "ok"}
62
+ >>> cli_output(v, OutputFormat.JSON).startswith('{"code"')
63
+ True
64
+ """
65
+ if format is OutputFormat.YAML:
66
+ return output_yaml(value)
67
+ if format is OutputFormat.PLAIN:
68
+ return output_plain(value)
69
+ return output_json(value)
70
+
71
+
72
+ def build_cli_error(message: str) -> dict:
73
+ """Build a standard CLI parse error value.
74
+
75
+ Use when argument parsing fails or a flag value is invalid.
76
+ Print with output_json and exit with code 2.
77
+
78
+ >>> v = build_cli_error("--output: invalid value 'xml'")
79
+ >>> v["code"]
80
+ 'error'
81
+ >>> v["error_code"]
82
+ 'invalid_request'
83
+ >>> v["retryable"]
84
+ False
85
+ """
86
+ return {
87
+ "code": "error",
88
+ "error_code": "invalid_request",
89
+ "error": message,
90
+ "retryable": False,
91
+ "trace": {"duration_ms": 0},
92
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/cmnspore/agent-first-data
@@ -74,7 +74,7 @@ Plain: args.input_path=/data/backup.tar.gz code=log event=startup config.max_fil
74
74
 
75
75
  ## API Reference
76
76
 
77
- Total: **8 public APIs** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility)
77
+ Total: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
78
78
 
79
79
  ### Protocol Builders (returns dict)
80
80
 
@@ -187,6 +187,41 @@ assert parse_size("1.5K") == 1536
187
187
  assert parse_size("512") == 512
188
188
  ```
189
189
 
190
+ ### CLI Helpers (for tools built on AFDATA)
191
+
192
+ Shared helpers that prevent flag-parsing drift between CLI tools. Use these instead of reimplementing `--output` and `--log` handling in each tool.
193
+
194
+ ```python
195
+ class OutputFormat(enum.Enum): # JSON="json", YAML="yaml", PLAIN="plain"
196
+
197
+ cli_parse_output(s: str) -> OutputFormat # Parse --output flag; raises ValueError on unknown
198
+ cli_parse_log_filters(entries: list[str]) -> list[str] # Normalize --log: trim, lowercase, dedup, remove empty
199
+ cli_output(value: Any, format: OutputFormat) -> str # Dispatch to output_json/yaml/plain
200
+ build_cli_error(message: str) -> dict # {code:"error", error_code:"invalid_request", retryable:False, trace:{duration_ms:0}}
201
+ ```
202
+
203
+ **Canonical pattern** — parse all flags before doing work, emit JSONL errors to stdout:
204
+
205
+ ```python
206
+ import sys
207
+ from agent_first_data import (
208
+ OutputFormat, cli_parse_output, cli_parse_log_filters,
209
+ cli_output, build_cli_error, output_json,
210
+ )
211
+
212
+ try:
213
+ fmt = cli_parse_output(args.output)
214
+ except ValueError as e:
215
+ print(output_json(build_cli_error(str(e))))
216
+ sys.exit(2)
217
+
218
+ log = cli_parse_log_filters(args.log.split(",") if args.log else [])
219
+ # ... do work ...
220
+ print(cli_output(result, fmt))
221
+ ```
222
+
223
+ See `examples/agent_cli.py` for the complete working example (`pytest examples/agent_cli.py`).
224
+
190
225
  ## Usage Examples
191
226
 
192
227
  ### Example 1: REST API
@@ -276,6 +311,7 @@ result = build_json_ok(
276
311
  )
277
312
 
278
313
  # Print JSONL to stdout (secrets redacted, one JSON object per line)
314
+ # Channel policy: machine-readable protocol/log events must not use stderr.
279
315
  print(output_json(result))
280
316
  # {"code":"ok","result":{"status":"success"},"trace":{"api_key_secret":"***","duration_ms":250}}
281
317
  ```
@@ -2,10 +2,13 @@ README.md
2
2
  pyproject.toml
3
3
  agent_first_data/__init__.py
4
4
  agent_first_data/afdata_logging.py
5
+ agent_first_data/cli.py
5
6
  agent_first_data/format.py
6
7
  agent_first_data.egg-info/PKG-INFO
7
8
  agent_first_data.egg-info/SOURCES.txt
8
9
  agent_first_data.egg-info/dependency_links.txt
9
10
  agent_first_data.egg-info/top_level.txt
10
11
  tests/test_afdata_logging.py
11
- tests/test_format.py
12
+ tests/test_cli.py
13
+ tests/test_format.py
14
+ tests/test_no_stderr_policy.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.3.0"
3
+ version = "0.4.0"
4
4
  description = "Agent-First Data (AFDATA) — suffix-driven output formatting and protocol templates for AI agents"
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -0,0 +1,98 @@
1
+ """Tests for agent_first_data CLI helpers."""
2
+ import pytest
3
+ from agent_first_data import (
4
+ OutputFormat,
5
+ cli_parse_output,
6
+ cli_parse_log_filters,
7
+ cli_output,
8
+ build_cli_error,
9
+ output_json,
10
+ )
11
+
12
+
13
+ # ── cli_parse_output ──────────────────────────────────────────────────────────
14
+
15
+ def test_parse_output_all_formats():
16
+ assert cli_parse_output("json") is OutputFormat.JSON
17
+ assert cli_parse_output("yaml") is OutputFormat.YAML
18
+ assert cli_parse_output("plain") is OutputFormat.PLAIN
19
+
20
+
21
+ def test_parse_output_rejects_unknown():
22
+ with pytest.raises(ValueError):
23
+ cli_parse_output("xml")
24
+ with pytest.raises(ValueError):
25
+ cli_parse_output("JSON")
26
+ with pytest.raises(ValueError):
27
+ cli_parse_output("")
28
+
29
+
30
+ def test_parse_output_error_contains_value():
31
+ with pytest.raises(ValueError, match="toml"):
32
+ cli_parse_output("toml")
33
+ with pytest.raises(ValueError, match="json"):
34
+ cli_parse_output("toml")
35
+
36
+
37
+ # ── cli_parse_log_filters ─────────────────────────────────────────────────────
38
+
39
+ def test_parse_log_filters_trims_and_lowercases():
40
+ assert cli_parse_log_filters([" Query ", "ERROR"]) == ["query", "error"]
41
+
42
+
43
+ def test_parse_log_filters_deduplicates():
44
+ assert cli_parse_log_filters(["query", "error", "Query", "query"]) == ["query", "error"]
45
+
46
+
47
+ def test_parse_log_filters_removes_empty():
48
+ assert cli_parse_log_filters(["", "query", " "]) == ["query"]
49
+
50
+
51
+ def test_parse_log_filters_empty_list():
52
+ assert cli_parse_log_filters([]) == []
53
+
54
+
55
+ def test_parse_log_filters_preserves_order():
56
+ assert cli_parse_log_filters(["startup", "request", "retry"]) == ["startup", "request", "retry"]
57
+
58
+
59
+ # ── build_cli_error ───────────────────────────────────────────────────────────
60
+
61
+ def test_build_cli_error_required_fields():
62
+ v = build_cli_error("missing --sql")
63
+ assert v["code"] == "error"
64
+ assert v["error_code"] == "invalid_request"
65
+ assert v["error"] == "missing --sql"
66
+ assert v["retryable"] is False
67
+ assert v["trace"]["duration_ms"] == 0
68
+
69
+
70
+ def test_build_cli_error_is_valid_json():
71
+ import json
72
+ v = build_cli_error("oops")
73
+ s = output_json(v)
74
+ parsed = json.loads(s)
75
+ assert parsed["code"] == "error"
76
+
77
+
78
+ # ── cli_output ────────────────────────────────────────────────────────────────
79
+
80
+ def test_cli_output_dispatches_json():
81
+ v = {"code": "ok", "size_bytes": 1024}
82
+ out = cli_output(v, OutputFormat.JSON)
83
+ assert "size_bytes" in out # json: raw keys, no suffix processing
84
+ assert "\n" not in out
85
+
86
+
87
+ def test_cli_output_dispatches_yaml():
88
+ v = {"code": "ok", "size_bytes": 1024}
89
+ out = cli_output(v, OutputFormat.YAML)
90
+ assert out.startswith("---")
91
+ assert "size:" in out # yaml: suffix stripped
92
+
93
+
94
+ def test_cli_output_dispatches_plain():
95
+ v = {"code": "ok"}
96
+ out = cli_output(v, OutputFormat.PLAIN)
97
+ assert "\n" not in out
98
+ assert "code=ok" in out
@@ -0,0 +1,23 @@
1
+ """Policy test: runtime library sources must not emit protocol/log events to stderr."""
2
+
3
+ from pathlib import Path
4
+ import re
5
+
6
+
7
+ DISALLOWED = re.compile(
8
+ r"\bsys\.stderr\b|\bfile\s*=\s*sys\.stderr\b|\bstderr\.write\s*\(",
9
+ )
10
+
11
+
12
+ def test_no_stderr_usage_in_runtime_sources() -> None:
13
+ root = Path(__file__).resolve().parents[1] / "agent_first_data"
14
+ files = sorted(root.glob("*.py"))
15
+ assert files, "no python source files found"
16
+
17
+ violations: list[str] = []
18
+ for path in files:
19
+ for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
20
+ if DISALLOWED.search(line):
21
+ violations.append(f"{path.name}:{lineno}: {line.strip()}")
22
+
23
+ assert not violations, "stderr usage is disallowed:\n" + "\n".join(violations)