agent-first-data 0.12.2__tar.gz → 0.13.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.
Files changed (21) hide show
  1. agent_first_data-0.13.0/PKG-INFO +55 -0
  2. agent_first_data-0.13.0/README.md +46 -0
  3. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/agent_first_data/__init__.py +6 -4
  4. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/agent_first_data/afdata_logging.py +64 -24
  5. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/agent_first_data/cli.py +1 -15
  6. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/agent_first_data/format.py +196 -85
  7. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/agent_first_data/skill.py +157 -40
  8. agent_first_data-0.13.0/agent_first_data.egg-info/PKG-INFO +55 -0
  9. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/pyproject.toml +1 -1
  10. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/tests/test_afdata_logging.py +82 -7
  11. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/tests/test_cli.py +3 -3
  12. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/tests/test_format.py +10 -9
  13. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/tests/test_skill.py +110 -5
  14. agent_first_data-0.12.2/PKG-INFO +0 -591
  15. agent_first_data-0.12.2/README.md +0 -582
  16. agent_first_data-0.12.2/agent_first_data.egg-info/PKG-INFO +0 -591
  17. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/agent_first_data.egg-info/SOURCES.txt +0 -0
  18. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
  19. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/agent_first_data.egg-info/top_level.txt +0 -0
  20. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/setup.cfg +0 -0
  21. {agent_first_data-0.12.2 → agent_first_data-0.13.0}/tests/test_no_stderr_policy.py +0 -0
@@ -0,0 +1,55 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-first-data
3
+ Version: 0.13.0
4
+ Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
+ License-Expression: MIT
6
+ Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
7
+ Requires-Python: >=3.9
8
+ Description-Content-Type: text/markdown
9
+
10
+ # Agent-First Data for Python
11
+
12
+ ```bash
13
+ pip install agent-first-data
14
+ ```
15
+
16
+ ```python
17
+ from agent_first_data import output_json, output_plain
18
+
19
+ value = {
20
+ "code": "ok",
21
+ "result": {
22
+ "api_key_secret": "sk-123",
23
+ "latency_ms": 1280,
24
+ "db_url": "postgres://user:p@ss@db/app?token_secret=abc",
25
+ },
26
+ }
27
+
28
+ print(output_json(value))
29
+ print(output_plain(value))
30
+ ```
31
+
32
+ Useful names use Python casing: `output_json`, `output_yaml`, `output_plain`, `output_json_with_options`, `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `parse_size`, `normalize_utc_offset`, `cli_parse_output`, `cli_output`, and `build_cli_error`.
33
+
34
+ Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
35
+
36
+ ```python
37
+ from agent_first_data import init_logging_json
38
+
39
+ init_logging_json("INFO", secret_names=("authorization",))
40
+ ```
41
+
42
+ ## Behavior Notes
43
+
44
+ - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
45
+ - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
46
+ - YAML/plain quote and escape keys as well as values, sort by UTF-16 code unit order, and render nested objects in arrays as canonical JSON.
47
+ - Logging records use `code: "log"` plus a separate `level` field, so error-level logs are not terminal protocol errors.
48
+ - `build_cli_error(message, hint?)` returns `{code:"error", error: message, hint?}` only.
49
+
50
+ ## Reference
51
+
52
+ - Full convention and API groups: [../docs/overview.md](../docs/overview.md)
53
+ - Formal cross-language contract: [../spec/agent-first-data.md](../spec/agent-first-data.md)
54
+ - Conformance fixtures: [../spec/fixtures](../spec/fixtures)
55
+ - Agent skill: [../skills/agent-first-data.md](../skills/agent-first-data.md)
@@ -0,0 +1,46 @@
1
+ # Agent-First Data for Python
2
+
3
+ ```bash
4
+ pip install agent-first-data
5
+ ```
6
+
7
+ ```python
8
+ from agent_first_data import output_json, output_plain
9
+
10
+ value = {
11
+ "code": "ok",
12
+ "result": {
13
+ "api_key_secret": "sk-123",
14
+ "latency_ms": 1280,
15
+ "db_url": "postgres://user:p@ss@db/app?token_secret=abc",
16
+ },
17
+ }
18
+
19
+ print(output_json(value))
20
+ print(output_plain(value))
21
+ ```
22
+
23
+ Useful names use Python casing: `output_json`, `output_yaml`, `output_plain`, `output_json_with_options`, `redacted_value`, `redact_secrets_in_place`, `redact_url_secrets`, `parse_size`, `normalize_utc_offset`, `cli_parse_output`, `cli_output`, and `build_cli_error`.
24
+
25
+ Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
26
+
27
+ ```python
28
+ from agent_first_data import init_logging_json
29
+
30
+ init_logging_json("INFO", secret_names=("authorization",))
31
+ ```
32
+
33
+ ## Behavior Notes
34
+
35
+ - Default redaction replaces every `_secret` or configured secret-name subtree with `***`, including objects and arrays.
36
+ - `_url` fields scrub userinfo passwords and secret-named query parameters; surrounding whitespace is trimmed and internal whitespace redacts the whole field.
37
+ - YAML/plain quote and escape keys as well as values, sort by UTF-16 code unit order, and render nested objects in arrays as canonical JSON.
38
+ - Logging records use `code: "log"` plus a separate `level` field, so error-level logs are not terminal protocol errors.
39
+ - `build_cli_error(message, hint?)` returns `{code:"error", error: message, hint?}` only.
40
+
41
+ ## Reference
42
+
43
+ - Full convention and API groups: [../docs/overview.md](../docs/overview.md)
44
+ - Formal cross-language contract: [../spec/agent-first-data.md](../spec/agent-first-data.md)
45
+ - Conformance fixtures: [../spec/fixtures](../spec/fixtures)
46
+ - Agent skill: [../skills/agent-first-data.md](../skills/agent-first-data.md)
@@ -15,14 +15,15 @@ from agent_first_data.format import (
15
15
  output_yaml_with_options,
16
16
  output_plain,
17
17
  output_plain_with_options,
18
- internal_redact_secrets,
19
- internal_redact_secrets_with_options,
18
+ redact_secrets_in_place,
19
+ redact_secrets_in_place_with_options,
20
20
  redacted_value,
21
21
  redacted_value_with,
22
22
  redacted_value_with_options,
23
23
  redact_url_secrets,
24
24
  redact_url_secrets_with_options,
25
25
  parse_size,
26
+ normalize_utc_offset,
26
27
  )
27
28
 
28
29
  from agent_first_data.afdata_logging import (
@@ -74,14 +75,15 @@ __all__ = [
74
75
  "output_yaml_with_options",
75
76
  "output_plain",
76
77
  "output_plain_with_options",
77
- "internal_redact_secrets",
78
- "internal_redact_secrets_with_options",
78
+ "redact_secrets_in_place",
79
+ "redact_secrets_in_place_with_options",
79
80
  "redacted_value",
80
81
  "redacted_value_with",
81
82
  "redacted_value_with_options",
82
83
  "redact_url_secrets",
83
84
  "redact_url_secrets_with_options",
84
85
  "parse_size",
86
+ "normalize_utc_offset",
85
87
  "AfdataHandler",
86
88
  "init_logging_json",
87
89
  "init_logging_plain",
@@ -21,13 +21,19 @@ Usage:
21
21
  import logging
22
22
  import sys
23
23
  from contextvars import ContextVar, Token
24
- from typing import Any
24
+ from typing import Any, Sequence
25
25
 
26
- from agent_first_data.format import output_json, output_plain, output_yaml
26
+ from agent_first_data.format import (
27
+ OutputOptions,
28
+ RedactionOptions,
29
+ output_json_with_options,
30
+ output_plain_with_options,
31
+ output_yaml_with_options,
32
+ )
27
33
 
28
34
  _span_fields: ContextVar[dict[str, Any]] = ContextVar("afdata_span", default={})
29
35
 
30
- _LEVEL_TO_CODE = {
36
+ _LEVEL_TO_NAME = {
31
37
  "CRITICAL": "error",
32
38
  "ERROR": "error",
33
39
  "WARNING": "warn",
@@ -44,17 +50,24 @@ class AfdataHandler(logging.Handler):
44
50
  Formats output using the library's own output_json/output_plain/output_yaml.
45
51
  """
46
52
 
47
- def __init__(self, format: str = "json") -> None:
53
+ def __init__(
54
+ self,
55
+ format: str = "json",
56
+ redaction: RedactionOptions | None = None,
57
+ secret_names: Sequence[str] | None = None,
58
+ ) -> None:
48
59
  super().__init__()
49
60
  if format not in ("json", "plain", "yaml"):
50
61
  raise ValueError(f"Unknown format: {format!r}, expected json/plain/yaml")
51
62
  self._format = format
63
+ self._redaction = _redaction_options(redaction, secret_names)
52
64
 
53
65
  def emit(self, record: logging.LogRecord) -> None:
54
66
  entry: dict[str, Any] = {
55
67
  "timestamp_epoch_ms": int(record.created * 1000),
56
68
  "message": record.getMessage(),
57
- "target": record.name,
69
+ "code": "log",
70
+ "level": _LEVEL_TO_NAME.get(record.levelname, "info"),
58
71
  }
59
72
 
60
73
  # Span fields (from contextvars, async-safe)
@@ -62,26 +75,23 @@ class AfdataHandler(logging.Handler):
62
75
  if span_data:
63
76
  entry.update(span_data)
64
77
 
65
- # Event fields (passed via extra= in logging calls)
66
- has_code = False
78
+ # Event fields (passed via extra= in logging calls). The protocol code
79
+ # is always "log"; use event for business categorization.
67
80
  extra = getattr(record, "_afdata_fields", None)
68
81
  if extra:
69
82
  for k, v in extra.items():
70
83
  if k == "code":
71
- has_code = True
84
+ continue
72
85
  entry[k] = v
73
86
 
74
- # Default code from level
75
- if not has_code:
76
- entry["code"] = _LEVEL_TO_CODE.get(record.levelname, "info")
77
-
78
- # Format using the library's own output functions
87
+ # Format using the library's own output functions.
88
+ options = OutputOptions(redaction=self._redaction)
79
89
  if self._format == "plain":
80
- line = output_plain(entry)
90
+ line = output_plain_with_options(entry, options)
81
91
  elif self._format == "yaml":
82
- line = output_yaml(entry)
92
+ line = output_yaml_with_options(entry, options)
83
93
  else:
84
- line = output_json(entry)
94
+ line = output_json_with_options(entry, options)
85
95
 
86
96
  sys.stdout.write(line + "\n")
87
97
  sys.stdout.flush()
@@ -100,26 +110,56 @@ class _AfdataLoggerAdapter(logging.LoggerAdapter):
100
110
  return msg, kwargs
101
111
 
102
112
 
103
- def _init_with_format(format: str, level: str = "INFO") -> None:
104
- handler = AfdataHandler(format=format)
113
+ def _redaction_options(
114
+ redaction: RedactionOptions | None,
115
+ secret_names: Sequence[str] | None,
116
+ ) -> RedactionOptions:
117
+ if redaction is not None and secret_names is not None:
118
+ raise ValueError("Pass either redaction or secret_names, not both")
119
+ if redaction is not None:
120
+ return redaction
121
+ if secret_names is not None:
122
+ return RedactionOptions(secret_names=tuple(secret_names))
123
+ return RedactionOptions()
124
+
125
+
126
+ def _init_with_format(
127
+ format: str,
128
+ level: str = "INFO",
129
+ redaction: RedactionOptions | None = None,
130
+ secret_names: Sequence[str] | None = None,
131
+ ) -> None:
132
+ handler = AfdataHandler(format=format, redaction=redaction, secret_names=secret_names)
105
133
  root = logging.getLogger()
106
134
  root.handlers = [handler]
107
135
  root.setLevel(getattr(logging, level.upper(), logging.INFO))
108
136
 
109
137
 
110
- def init_json(level: str = "INFO") -> None:
138
+ def init_json(
139
+ level: str = "INFO",
140
+ redaction: RedactionOptions | None = None,
141
+ secret_names: Sequence[str] | None = None,
142
+ ) -> None:
111
143
  """Initialize the root logger with AFDATA JSON output to stdout."""
112
- _init_with_format("json", level)
144
+ _init_with_format("json", level, redaction=redaction, secret_names=secret_names)
113
145
 
114
146
 
115
- def init_plain(level: str = "INFO") -> None:
147
+ def init_plain(
148
+ level: str = "INFO",
149
+ redaction: RedactionOptions | None = None,
150
+ secret_names: Sequence[str] | None = None,
151
+ ) -> None:
116
152
  """Initialize the root logger with AFDATA plain/logfmt output to stdout."""
117
- _init_with_format("plain", level)
153
+ _init_with_format("plain", level, redaction=redaction, secret_names=secret_names)
118
154
 
119
155
 
120
- def init_yaml(level: str = "INFO") -> None:
156
+ def init_yaml(
157
+ level: str = "INFO",
158
+ redaction: RedactionOptions | None = None,
159
+ secret_names: Sequence[str] | None = None,
160
+ ) -> None:
121
161
  """Initialize the root logger with AFDATA YAML output to stdout."""
122
- _init_with_format("yaml", level)
162
+ _init_with_format("yaml", level, redaction=redaction, secret_names=secret_names)
123
163
 
124
164
 
125
165
  def get_logger(name: str, **fields: Any) -> logging.LoggerAdapter:
@@ -98,22 +98,8 @@ def build_cli_error(message: str, hint: str | None = None) -> dict:
98
98
 
99
99
  Use when argument parsing fails or a flag value is invalid.
100
100
  Print with output_json and exit with code 2.
101
-
102
- >>> v = build_cli_error("--output: invalid value 'xml'")
103
- >>> v["code"]
104
- 'error'
105
- >>> v["error_code"]
106
- 'invalid_request'
107
- >>> v["retryable"]
108
- False
109
101
  """
110
- m: dict = {
111
- "code": "error",
112
- "error_code": "invalid_request",
113
- "error": message,
114
- "retryable": False,
115
- "trace": {"duration_ms": 0},
116
- }
102
+ m: dict = {"code": "error", "error": message}
117
103
  if hint is not None:
118
104
  m["hint"] = hint
119
105
  return m