agent-first-data 0.1.0__py3-none-any.whl

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.
@@ -0,0 +1,29 @@
1
+ """Agent-First Data (AFD) — suffix-driven output formatting and protocol templates."""
2
+
3
+ from agent_first_data.format import (
4
+ OutputFormat,
5
+ to_yaml,
6
+ to_plain,
7
+ redact_secrets,
8
+ ok,
9
+ ok_trace,
10
+ error,
11
+ error_trace,
12
+ startup,
13
+ status,
14
+ parse_size,
15
+ )
16
+
17
+ __all__ = [
18
+ "OutputFormat",
19
+ "to_yaml",
20
+ "to_plain",
21
+ "redact_secrets",
22
+ "ok",
23
+ "ok_trace",
24
+ "error",
25
+ "error_trace",
26
+ "startup",
27
+ "status",
28
+ "parse_size",
29
+ ]
@@ -0,0 +1,352 @@
1
+ """AFD output formatting: JSON, YAML, plain text with suffix-driven transforms."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from datetime import datetime, timezone
7
+ from enum import Enum
8
+ from typing import Any
9
+
10
+
11
+ class OutputFormat(Enum):
12
+ JSON = "json"
13
+ YAML = "yaml"
14
+ PLAIN = "plain"
15
+
16
+ def format(self, value: Any) -> str:
17
+ if self is OutputFormat.JSON:
18
+ return json.dumps(value, ensure_ascii=False, separators=(",", ":"))
19
+ if self is OutputFormat.YAML:
20
+ return to_yaml(value)
21
+ return to_plain(value)
22
+
23
+ def format_pretty(self, value: Any) -> str:
24
+ if self is OutputFormat.JSON:
25
+ return json.dumps(value, ensure_ascii=False, indent=2)
26
+ if self is OutputFormat.YAML:
27
+ return to_yaml(value)
28
+ return to_plain(value)
29
+
30
+
31
+ # ═══════════════════════════════════════════
32
+ # YAML
33
+ # ═══════════════════════════════════════════
34
+
35
+
36
+ def to_yaml(value: Any) -> str:
37
+ lines = ["---"]
38
+ _render_yaml(value, 0, lines)
39
+ return "\n".join(lines)
40
+
41
+
42
+ def _render_yaml(value: Any, indent: int, lines: list[str]) -> None:
43
+ prefix = " " * indent
44
+ if isinstance(value, dict):
45
+ for k, v in sorted(value.items(), key=lambda kv: kv[0].encode("utf-16-be")):
46
+ if isinstance(v, dict):
47
+ if v:
48
+ lines.append(f"{prefix}{k}:")
49
+ _render_yaml(v, indent + 1, lines)
50
+ else:
51
+ lines.append(f"{prefix}{k}: {{}}")
52
+ elif isinstance(v, list):
53
+ if not v:
54
+ lines.append(f"{prefix}{k}: []")
55
+ else:
56
+ lines.append(f"{prefix}{k}:")
57
+ for item in v:
58
+ if isinstance(item, dict):
59
+ lines.append(f"{prefix} -")
60
+ _render_yaml(item, indent + 2, lines)
61
+ else:
62
+ lines.append(f"{prefix} - {_yaml_scalar(item)}")
63
+ else:
64
+ lines.append(f"{prefix}{k}: {_yaml_scalar(v)}")
65
+ else:
66
+ lines.append(f"{prefix}{_yaml_scalar(value)}")
67
+
68
+
69
+ def _yaml_scalar(value: Any) -> str:
70
+ if isinstance(value, str):
71
+ escaped = value.replace("\\", "\\\\").replace('"', '\\"')
72
+ escaped = escaped.replace("\n", "\\n").replace("\r", "\\r").replace("\t", "\\t")
73
+ return f'"{escaped}"'
74
+ if value is None:
75
+ return "null"
76
+ if isinstance(value, bool):
77
+ return "true" if value else "false"
78
+ if isinstance(value, (int, float)):
79
+ return str(value)
80
+ escaped = str(value).replace('"', '\\"')
81
+ return f'"{escaped}"'
82
+
83
+
84
+ # ═══════════════════════════════════════════
85
+ # Plain
86
+ # ═══════════════════════════════════════════
87
+
88
+
89
+ def to_plain(value: Any) -> str:
90
+ lines: list[str] = []
91
+ _render_plain(value, 0, lines)
92
+ return "\n".join(lines)
93
+
94
+
95
+ def _render_plain(value: Any, indent: int, lines: list[str]) -> None:
96
+ prefix = " " * indent
97
+ if isinstance(value, dict):
98
+ for k, v in sorted(value.items(), key=lambda kv: kv[0].encode("utf-16-be")):
99
+ if isinstance(v, dict):
100
+ lines.append(f"{prefix}{k}:")
101
+ _render_plain(v, indent + 1, lines)
102
+ elif isinstance(v, list):
103
+ if not v:
104
+ lines.append(f"{prefix}{k}: []")
105
+ elif all(not isinstance(i, (dict, list)) for i in v):
106
+ lines.append(f"{prefix}{k}:")
107
+ for item in v:
108
+ lines.append(f"{prefix} - {_plain_scalar(item)}")
109
+ else:
110
+ lines.append(f"{prefix}{k}:")
111
+ for item in v:
112
+ if isinstance(item, dict):
113
+ lines.append(f"{prefix} -")
114
+ _render_plain(item, indent + 2, lines)
115
+ else:
116
+ lines.append(f"{prefix} - {_plain_scalar(item)}")
117
+ else:
118
+ lines.append(f"{prefix}{k}: {_format_plain_field(k, v)}")
119
+ else:
120
+ lines.append(f"{prefix}{_plain_scalar(value)}")
121
+
122
+
123
+ def _format_plain_field(key: str, value: Any) -> str:
124
+ lower = key.lower()
125
+
126
+ # Secret — always redact
127
+ if lower.endswith("_secret"):
128
+ return "***"
129
+
130
+ # Timestamps → RFC 3339
131
+ if lower.endswith("_epoch_ms"):
132
+ if isinstance(value, int) and not isinstance(value, bool):
133
+ return _format_rfc3339_ms(value)
134
+ if lower.endswith("_epoch_s"):
135
+ if isinstance(value, int) and not isinstance(value, bool):
136
+ return _format_rfc3339_ms(value * 1000)
137
+ if lower.endswith("_epoch_ns"):
138
+ if isinstance(value, int) and not isinstance(value, bool):
139
+ return _format_rfc3339_ms(value // 1_000_000)
140
+ if lower.endswith("_rfc3339"):
141
+ return _plain_scalar(value)
142
+
143
+ # Size
144
+ if lower.endswith("_bytes"):
145
+ if isinstance(value, int) and not isinstance(value, bool):
146
+ return _format_bytes_human(value)
147
+
148
+ # Percentage
149
+ if lower.endswith("_percent"):
150
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
151
+ return f"{_plain_scalar(value)}%"
152
+
153
+ # Currency — Bitcoin
154
+ if lower.endswith("_msats"):
155
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
156
+ return f"{_plain_scalar(value)}msats"
157
+ if lower.endswith("_sats"):
158
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
159
+ return f"{_plain_scalar(value)}sats"
160
+ if lower.endswith("_btc"):
161
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
162
+ return f"{_plain_scalar(value)} BTC"
163
+
164
+ # Currency — Fiat with symbol
165
+ if lower.endswith("_usd_cents"):
166
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
167
+ return f"${value // 100}.{value % 100:02d}"
168
+ if lower.endswith("_eur_cents"):
169
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
170
+ return f"\u20ac{value // 100}.{value % 100:02d}"
171
+ if lower.endswith("_jpy"):
172
+ if isinstance(value, int) and not isinstance(value, bool) and value >= 0:
173
+ return f"\u00a5{_format_with_commas(value)}"
174
+ # Currency — Generic _{code}_cents
175
+ if lower.endswith("_cents"):
176
+ code = _extract_currency_code(lower)
177
+ if code and isinstance(value, int) and not isinstance(value, bool) and value >= 0:
178
+ return f"{value // 100}.{value % 100:02d} {code.upper()}"
179
+
180
+ # Duration — long units
181
+ if lower.endswith("_minutes"):
182
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
183
+ return f"{_plain_scalar(value)} minutes"
184
+ if lower.endswith("_hours"):
185
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
186
+ return f"{_plain_scalar(value)} hours"
187
+ if lower.endswith("_days"):
188
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
189
+ return f"{_plain_scalar(value)} days"
190
+
191
+ # Duration — ms
192
+ if lower.endswith("_ms") and not lower.endswith("_epoch_ms"):
193
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
194
+ if value >= 1000:
195
+ return f"{value / 1000:.2f}s"
196
+ return f"{_plain_scalar(value)}ms"
197
+
198
+ # Duration — ns, us, s
199
+ if lower.endswith("_ns") and not lower.endswith("_epoch_ns"):
200
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
201
+ return f"{_plain_scalar(value)}ns"
202
+ if lower.endswith("_us"):
203
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
204
+ return f"{_plain_scalar(value)}\u03bcs"
205
+ if lower.endswith("_s") and not lower.endswith("_epoch_s"):
206
+ if isinstance(value, (int, float)) and not isinstance(value, bool):
207
+ return f"{_plain_scalar(value)}s"
208
+
209
+ return _plain_scalar(value)
210
+
211
+
212
+ def _plain_scalar(value: Any) -> str:
213
+ if isinstance(value, str):
214
+ return value
215
+ if value is None:
216
+ return "null"
217
+ if isinstance(value, bool):
218
+ return "true" if value else "false"
219
+ if isinstance(value, (int, float)):
220
+ return str(value)
221
+ return str(value)
222
+
223
+
224
+ # ═══════════════════════════════════════════
225
+ # Secret redaction
226
+ # ═══════════════════════════════════════════
227
+
228
+
229
+ def redact_secrets(value: Any) -> Any:
230
+ """Walk a dict/list tree and redact any key ending in '_secret'."""
231
+ if isinstance(value, dict):
232
+ for k in value:
233
+ if k.lower().endswith("_secret") and isinstance(value[k], str):
234
+ value[k] = "***"
235
+ redact_secrets(value[k])
236
+ elif isinstance(value, list):
237
+ for item in value:
238
+ redact_secrets(item)
239
+ return value
240
+
241
+
242
+ # ═══════════════════════════════════════════
243
+ # AFD Protocol templates
244
+ # ═══════════════════════════════════════════
245
+
246
+
247
+ def ok(result: Any) -> dict:
248
+ return {"code": "ok", "result": result}
249
+
250
+
251
+ def ok_trace(result: Any, trace: Any) -> dict:
252
+ return {"code": "ok", "result": result, "trace": trace}
253
+
254
+
255
+ def error(message: str) -> dict:
256
+ return {"code": "error", "error": message}
257
+
258
+
259
+ def error_trace(message: str, trace: Any) -> dict:
260
+ return {"code": "error", "error": message, "trace": trace}
261
+
262
+
263
+ def startup(config: Any, args: Any, env: Any) -> dict:
264
+ return {"code": "startup", "config": config, "args": args, "env": env}
265
+
266
+
267
+ def status(code: str, fields: dict | None = None) -> dict:
268
+ result = dict(fields) if fields else {}
269
+ result["code"] = code
270
+ return result
271
+
272
+
273
+ # ═══════════════════════════════════════════
274
+ # Helpers
275
+ # ═══════════════════════════════════════════
276
+
277
+
278
+ def _format_rfc3339_ms(ms: int) -> str:
279
+ try:
280
+ dt = datetime.fromtimestamp(ms / 1000, tz=timezone.utc)
281
+ return dt.strftime("%Y-%m-%dT%H:%M:%S.") + f"{ms % 1000:03d}Z"
282
+ except (OSError, OverflowError, ValueError):
283
+ return str(ms)
284
+
285
+
286
+ def _format_bytes_human(n: int) -> str:
287
+ KB = 1024.0
288
+ MB = KB * 1024
289
+ GB = MB * 1024
290
+ TB = GB * 1024
291
+ sign = "-" if n < 0 else ""
292
+ b = float(abs(n))
293
+ if b >= TB:
294
+ return f"{sign}{b / TB:.1f}TB"
295
+ if b >= GB:
296
+ return f"{sign}{b / GB:.1f}GB"
297
+ if b >= MB:
298
+ return f"{sign}{b / MB:.1f}MB"
299
+ if b >= KB:
300
+ return f"{sign}{b / KB:.1f}KB"
301
+ return f"{n}B"
302
+
303
+
304
+ def _format_with_commas(n: int) -> str:
305
+ return f"{n:,}"
306
+
307
+
308
+ def parse_size(s: str) -> int | None:
309
+ """Parse a human-readable size string into bytes.
310
+
311
+ Accepts bare numbers or numbers followed by a unit letter (B/K/M/G/T).
312
+ Case-insensitive. Trims whitespace. Returns None for invalid input.
313
+ """
314
+ _multipliers = {"b": 1, "k": 1024, "m": 1024**2, "g": 1024**3, "t": 1024**4}
315
+ s = s.strip()
316
+ if not s:
317
+ return None
318
+ last = s[-1].lower()
319
+ if last in _multipliers:
320
+ num_str = s[:-1]
321
+ mult = _multipliers[last]
322
+ elif last.isdigit() or last == ".":
323
+ num_str = s
324
+ mult = 1
325
+ else:
326
+ return None
327
+ if not num_str:
328
+ return None
329
+ try:
330
+ n = int(num_str)
331
+ if n < 0:
332
+ return None
333
+ return n * mult
334
+ except ValueError:
335
+ pass
336
+ try:
337
+ f = float(num_str)
338
+ if f < 0 or f != f: # NaN check
339
+ return None
340
+ return int(f * mult)
341
+ except (ValueError, OverflowError):
342
+ return None
343
+
344
+
345
+ def _extract_currency_code(key: str) -> str | None:
346
+ without_cents = key.removesuffix("_cents")
347
+ if without_cents == key:
348
+ return None
349
+ idx = without_cents.rfind("_")
350
+ if idx < 0:
351
+ return None
352
+ return without_cents[idx + 1:]
@@ -0,0 +1,6 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-first-data
3
+ Version: 0.1.0
4
+ Summary: Agent-First Data (AFD) — suffix-driven output formatting and protocol templates for AI agents
5
+ License-Expression: MIT
6
+ Requires-Python: >=3.9
@@ -0,0 +1,6 @@
1
+ agent_first_data/__init__.py,sha256=LIukT0rjWIorX01kpQEk0S4lm7CA8rh6JWqSiGYlYZI,479
2
+ agent_first_data/format.py,sha256=-SBwXMETymOeAoFlWDWnYm-aN4qJmb62_3TG6s8sZPk,12472
3
+ agent_first_data-0.1.0.dist-info/METADATA,sha256=HXv2RPwXeuMyiCnKbospgn7Z6Z0M-Fllm7tWubhCKMg,212
4
+ agent_first_data-0.1.0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
5
+ agent_first_data-0.1.0.dist-info/top_level.txt,sha256=0Q_fDDPFGTC7UC6NpN_BFW4M9z1r-mKy3Orh0B1-mR8,17
6
+ agent_first_data-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ agent_first_data