agent-first-data 0.4.2__tar.gz → 0.5.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.4.2
3
+ Version: 0.5.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,11 +74,11 @@ 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: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
77
+ Total: **13 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
78
78
 
79
79
  ### Protocol Builders (returns dict)
80
80
 
81
- Build AFDATA protocol structures. Return dict objects for API responses.
81
+ Build AFDATA protocol structures. Return dict objects for transport payloads.
82
82
 
83
83
  ```python
84
84
  # Success (result)
@@ -91,7 +91,7 @@ build_json_error(message: str, trace: Any = None) -> dict
91
91
  build_json(code: str, fields: Any, trace: Any = None) -> dict
92
92
  ```
93
93
 
94
- **Use case:** API responses (frameworks like FastAPI automatically serialize)
94
+ **Use case:** structured protocol payloads (frameworks automatically serialize)
95
95
 
96
96
  **Example:**
97
97
  ```python
@@ -128,14 +128,21 @@ not_found = build_json(
128
128
 
129
129
  ### CLI/Log Output (returns str)
130
130
 
131
- Format values for CLI output and logs. **All formats redact `_secret` fields.** YAML and Plain also strip suffixes from keys and format values for human readability.
131
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
132
132
 
133
133
  ```python
134
134
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
135
+ output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
135
136
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
136
137
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
137
138
  ```
138
139
 
140
+ ```python
141
+ class RedactionPolicy(enum.Enum):
142
+ RedactionTraceOnly = "RedactionTraceOnly"
143
+ RedactionNone = "RedactionNone"
144
+ ```
145
+
139
146
  **Example:**
140
147
  ```python
141
148
  from agent_first_data import *
@@ -65,11 +65,11 @@ 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: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
68
+ Total: **13 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
69
69
 
70
70
  ### Protocol Builders (returns dict)
71
71
 
72
- Build AFDATA protocol structures. Return dict objects for API responses.
72
+ Build AFDATA protocol structures. Return dict objects for transport payloads.
73
73
 
74
74
  ```python
75
75
  # Success (result)
@@ -82,7 +82,7 @@ build_json_error(message: str, trace: Any = None) -> dict
82
82
  build_json(code: str, fields: Any, trace: Any = None) -> dict
83
83
  ```
84
84
 
85
- **Use case:** API responses (frameworks like FastAPI automatically serialize)
85
+ **Use case:** structured protocol payloads (frameworks automatically serialize)
86
86
 
87
87
  **Example:**
88
88
  ```python
@@ -119,14 +119,21 @@ not_found = build_json(
119
119
 
120
120
  ### CLI/Log Output (returns str)
121
121
 
122
- Format values for CLI output and logs. **All formats redact `_secret` fields.** YAML and Plain also strip suffixes from keys and format values for human readability.
122
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
123
123
 
124
124
  ```python
125
125
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
126
+ output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
126
127
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
127
128
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
128
129
  ```
129
130
 
131
+ ```python
132
+ class RedactionPolicy(enum.Enum):
133
+ RedactionTraceOnly = "RedactionTraceOnly"
134
+ RedactionNone = "RedactionNone"
135
+ ```
136
+
130
137
  **Example:**
131
138
  ```python
132
139
  from agent_first_data import *
@@ -4,7 +4,9 @@ from agent_first_data.format import (
4
4
  build_json_ok,
5
5
  build_json_error,
6
6
  build_json,
7
+ RedactionPolicy,
7
8
  output_json,
9
+ output_json_with,
8
10
  output_yaml,
9
11
  output_plain,
10
12
  internal_redact_secrets,
@@ -33,7 +35,9 @@ __all__ = [
33
35
  "build_json_ok",
34
36
  "build_json_error",
35
37
  "build_json",
38
+ "RedactionPolicy",
36
39
  "output_json",
40
+ "output_json_with",
37
41
  "output_yaml",
38
42
  "output_plain",
39
43
  "internal_redact_secrets",
@@ -1,13 +1,14 @@
1
1
  """AFDATA output formatting and protocol templates.
2
2
 
3
- 8 public APIs: 3 protocol builders + 3 output formatters + 1 redaction + 1 utility.
3
+ 9 public APIs and 1 type: 3 protocol builders + 4 output formatters + 1 redaction + 1 utility + RedactionPolicy.
4
4
  """
5
5
 
6
6
  from __future__ import annotations
7
7
 
8
- import copy
9
8
  import json
9
+ import math
10
10
  from datetime import datetime, timezone
11
+ from enum import Enum
11
12
  from typing import Any
12
13
 
13
14
 
@@ -45,16 +46,28 @@ def build_json(code: str, fields: Any, trace: Any = None) -> dict:
45
46
  # Public API: Output Formatters
46
47
  # ═══════════════════════════════════════════
47
48
 
49
+ class RedactionPolicy(str, Enum):
50
+ RedactionTraceOnly = "RedactionTraceOnly"
51
+ RedactionNone = "RedactionNone"
52
+
48
53
 
49
54
  def output_json(value: Any) -> str:
50
55
  """Format as single-line JSON. Secrets redacted, original keys, raw values."""
51
- v = copy.deepcopy(value)
56
+ v = _sanitize_for_json(value)
52
57
  _redact_secrets(v)
53
58
  return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
54
59
 
55
60
 
61
+ def output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str:
62
+ """Format as single-line JSON with explicit redaction policy."""
63
+ v = _sanitize_for_json(value)
64
+ _apply_redaction_policy(v, redaction_policy)
65
+ return json.dumps(v, ensure_ascii=False, separators=(",", ":"))
66
+
67
+
56
68
  def output_yaml(value: Any) -> str:
57
69
  """Format as multi-line YAML. Keys stripped, values formatted, secrets redacted."""
70
+ value = _sanitize_for_json(value)
58
71
  lines = ["---"]
59
72
  _render_yaml_processed(value, 0, lines)
60
73
  return "\n".join(lines)
@@ -62,6 +75,7 @@ def output_yaml(value: Any) -> str:
62
75
 
63
76
  def output_plain(value: Any) -> str:
64
77
  """Format as single-line logfmt. Keys stripped, values formatted, secrets redacted."""
78
+ value = _sanitize_for_json(value)
65
79
  pairs: list[tuple[str, str]] = []
66
80
  _collect_plain_pairs(value, "", pairs)
67
81
  pairs.sort(key=lambda p: p[0].encode("utf-16-be"))
@@ -84,6 +98,17 @@ def internal_redact_secrets(value: Any) -> None:
84
98
  _redact_secrets(value)
85
99
 
86
100
 
101
+ def _apply_redaction_policy(value: Any, redaction_policy: RedactionPolicy) -> None:
102
+ if redaction_policy == RedactionPolicy.RedactionTraceOnly:
103
+ if isinstance(value, dict) and "trace" in value:
104
+ _redact_secrets(value["trace"])
105
+ return
106
+ if redaction_policy == RedactionPolicy.RedactionNone:
107
+ return
108
+ # Safety fallback for unknown values.
109
+ _redact_secrets(value)
110
+
111
+
87
112
  def parse_size(s: str) -> int | None:
88
113
  """Parse a human-readable size string into bytes.
89
114
 
@@ -126,6 +151,43 @@ def parse_size(s: str) -> int | None:
126
151
  # ═══════════════════════════════════════════
127
152
 
128
153
 
154
+ def _sanitize_for_json(value: Any, stack: set[int] | None = None) -> Any:
155
+ if stack is None:
156
+ stack = set()
157
+
158
+ if value is None or isinstance(value, (str, bool, int)):
159
+ return value
160
+ if isinstance(value, float):
161
+ if math.isfinite(value):
162
+ return value
163
+ return "<unsupported:float>"
164
+ if isinstance(value, BaseException):
165
+ return str(value)
166
+
167
+ if isinstance(value, dict):
168
+ obj_id = id(value)
169
+ if obj_id in stack:
170
+ return "<unsupported:circular>"
171
+ stack.add(obj_id)
172
+ out: dict[str, Any] = {}
173
+ for k, v in value.items():
174
+ key = k if isinstance(k, str) else str(k)
175
+ out[key] = _sanitize_for_json(v, stack)
176
+ stack.remove(obj_id)
177
+ return out
178
+
179
+ if isinstance(value, (list, tuple)):
180
+ obj_id = id(value)
181
+ if obj_id in stack:
182
+ return "<unsupported:circular>"
183
+ stack.add(obj_id)
184
+ out = [_sanitize_for_json(item, stack) for item in value]
185
+ stack.remove(obj_id)
186
+ return out
187
+
188
+ return f"<unsupported:{type(value).__name__}>"
189
+
190
+
129
191
  def _redact_secrets(value: Any) -> None:
130
192
  if isinstance(value, dict):
131
193
  for k in list(value.keys()):
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.4.2
3
+ Version: 0.5.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,11 +74,11 @@ 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: **12 public APIs and 1 type** + **AFDATA logging** (3 protocol builders + 3 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat`)
77
+ Total: **13 public APIs and 2 types** + **AFDATA logging** (3 protocol builders + 4 output functions + 1 internal + 1 utility + 4 CLI helpers + `OutputFormat` + `RedactionPolicy`)
78
78
 
79
79
  ### Protocol Builders (returns dict)
80
80
 
81
- Build AFDATA protocol structures. Return dict objects for API responses.
81
+ Build AFDATA protocol structures. Return dict objects for transport payloads.
82
82
 
83
83
  ```python
84
84
  # Success (result)
@@ -91,7 +91,7 @@ build_json_error(message: str, trace: Any = None) -> dict
91
91
  build_json(code: str, fields: Any, trace: Any = None) -> dict
92
92
  ```
93
93
 
94
- **Use case:** API responses (frameworks like FastAPI automatically serialize)
94
+ **Use case:** structured protocol payloads (frameworks automatically serialize)
95
95
 
96
96
  **Example:**
97
97
  ```python
@@ -128,14 +128,21 @@ not_found = build_json(
128
128
 
129
129
  ### CLI/Log Output (returns str)
130
130
 
131
- Format values for CLI output and logs. **All formats redact `_secret` fields.** YAML and Plain also strip suffixes from keys and format values for human readability.
131
+ Format values for CLI output and logs. `output_json` uses full `_secret` redaction by default. `output_json_with` supports explicit scoped policies. YAML and Plain always redact `_secret` and apply human-readable formatting.
132
132
 
133
133
  ```python
134
134
  output_json(value: Any) -> str # Single-line JSON, original keys, for programs/logs
135
+ output_json_with(value: Any, redaction_policy: RedactionPolicy) -> str
135
136
  output_yaml(value: Any) -> str # Multi-line YAML, keys stripped, values formatted
136
137
  output_plain(value: Any) -> str # Single-line logfmt, keys stripped, values formatted
137
138
  ```
138
139
 
140
+ ```python
141
+ class RedactionPolicy(enum.Enum):
142
+ RedactionTraceOnly = "RedactionTraceOnly"
143
+ RedactionNone = "RedactionNone"
144
+ ```
145
+
139
146
  **Example:**
140
147
  ```python
141
148
  from agent_first_data import *
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.4.2"
3
+ version = "0.5.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"
@@ -112,6 +112,12 @@ class TestCodeOverride:
112
112
  assert m["code"] == "log"
113
113
  assert m["event"] == "startup"
114
114
 
115
+ def test_exception_field_is_readable(self):
116
+ logger = make_logger("test_exc")
117
+ adapter = get_logger("test_exc")
118
+ m = capture_log(lambda: adapter.error("request failed", extra={"error": Exception("timeout")}))
119
+ assert m["error"] == "timeout"
120
+
115
121
 
116
122
  class TestGetLogger:
117
123
  def test_default_fields(self):
@@ -7,7 +7,10 @@ from agent_first_data import (
7
7
  build_json_ok,
8
8
  build_json_error,
9
9
  build_json,
10
+ RedactionPolicy,
10
11
  internal_redact_secrets,
12
+ output_json,
13
+ output_json_with,
11
14
  )
12
15
  from agent_first_data.format import (
13
16
  _format_bytes_human,
@@ -83,3 +86,52 @@ def test_helper_fixtures():
83
86
  elif name == "parse_size":
84
87
  got = parse_size(inp)
85
88
  assert got == expected, f"[helpers/{name}({inp!r})] got {got!r}"
89
+
90
+
91
+ def test_output_json_exception_field_is_readable():
92
+ out = output_json({"error": Exception("timeout")})
93
+ parsed = json.loads(out)
94
+ assert parsed["error"] == "timeout"
95
+
96
+
97
+ def test_output_json_unsupported_value_does_not_leak_secret():
98
+ class SecretRepr:
99
+ def __repr__(self) -> str:
100
+ return "Secret(sk-live-123)"
101
+
102
+ out = output_json({"meta": SecretRepr(), "api_key_secret": "sk-live-123"})
103
+ assert "sk-live-123" not in out
104
+ parsed = json.loads(out)
105
+ assert parsed["api_key_secret"] == "***"
106
+ assert parsed["meta"].startswith("<unsupported:")
107
+
108
+
109
+ def test_output_json_circular_reference():
110
+ v = {}
111
+ v["self"] = v
112
+ out = output_json(v)
113
+ parsed = json.loads(out)
114
+ assert parsed["self"] == "<unsupported:circular>"
115
+
116
+
117
+ def test_output_json_with_trace_only_redacts_only_trace():
118
+ out = output_json_with(
119
+ {
120
+ "code": "ok",
121
+ "result": {"api_key_secret": "sk-live-123"},
122
+ "trace": {"request_secret": "top-secret"},
123
+ },
124
+ RedactionPolicy.RedactionTraceOnly,
125
+ )
126
+ parsed = json.loads(out)
127
+ assert parsed["trace"]["request_secret"] == "***"
128
+ assert parsed["result"]["api_key_secret"] == "sk-live-123"
129
+
130
+
131
+ def test_output_json_with_none_keeps_secrets():
132
+ out = output_json_with(
133
+ {"api_key_secret": "sk-live-123"},
134
+ RedactionPolicy.RedactionNone,
135
+ )
136
+ parsed = json.loads(out)
137
+ assert parsed["api_key_secret"] == "sk-live-123"