agent-first-data 0.14.2__tar.gz → 0.15.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 (20) hide show
  1. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/PKG-INFO +3 -2
  2. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/README.md +2 -1
  3. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/__init__.py +17 -0
  4. agent_first_data-0.15.0/agent_first_data/stream_redirect.py +159 -0
  5. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/PKG-INFO +3 -2
  6. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/SOURCES.txt +3 -1
  7. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/pyproject.toml +1 -1
  8. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_no_stderr_policy.py +2 -0
  9. agent_first_data-0.15.0/tests/test_stream_redirect.py +47 -0
  10. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/afdata_logging.py +0 -0
  11. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/cli.py +0 -0
  12. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/format.py +0 -0
  13. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/skill.py +0 -0
  14. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
  15. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/top_level.txt +0 -0
  16. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/setup.cfg +0 -0
  17. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_afdata_logging.py +0 -0
  18. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_cli.py +0 -0
  19. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_format.py +0 -0
  20. {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_skill.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.14.2
3
+ Version: 0.15.0
4
4
  Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
@@ -29,7 +29,7 @@ print(output_json(value))
29
29
  print(output_plain(value))
30
30
  ```
31
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`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, and `cli_handle_version_or_continue`.
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`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `stream_redirect_config_from_raw_args`, and `install_stream_redirect_from_raw_args`.
33
33
 
34
34
  Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
35
35
 
@@ -47,6 +47,7 @@ init_logging_json("INFO", secret_names=("authorization",))
47
47
  - Logging records use `code: "log"` plus a separate `level` field, so error-level logs are not terminal protocol errors.
48
48
  - `build_cli_error(message, hint?)` returns `{code:"error", error: message, hint?}` only.
49
49
  - Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
50
+ - Use `install_stream_redirect_from_raw_args()` before version/help handling if a CLI exposes `--stdout-file` or `--stderr-file`; stderr is redirected as native diagnostics, not JSON.
50
51
 
51
52
  ## Reference
52
53
 
@@ -20,7 +20,7 @@ print(output_json(value))
20
20
  print(output_plain(value))
21
21
  ```
22
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`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, and `cli_handle_version_or_continue`.
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`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `stream_redirect_config_from_raw_args`, and `install_stream_redirect_from_raw_args`.
24
24
 
25
25
  Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
26
26
 
@@ -38,6 +38,7 @@ init_logging_json("INFO", secret_names=("authorization",))
38
38
  - Logging records use `code: "log"` plus a separate `level` field, so error-level logs are not terminal protocol errors.
39
39
  - `build_cli_error(message, hint?)` returns `{code:"error", error: message, hint?}` only.
40
40
  - Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
41
+ - Use `install_stream_redirect_from_raw_args()` before version/help handling if a CLI exposes `--stdout-file` or `--stderr-file`; stderr is redirected as native diagnostics, not JSON.
41
42
 
42
43
  ## Reference
43
44
 
@@ -65,6 +65,16 @@ from agent_first_data.skill import (
65
65
  run_skill_admin,
66
66
  )
67
67
 
68
+ from agent_first_data.stream_redirect import (
69
+ STDOUT_FILE_ARG,
70
+ STDERR_FILE_ARG,
71
+ StreamRedirectConfig,
72
+ InstalledStreamRedirect,
73
+ config_from_raw_args as stream_redirect_config_from_raw_args,
74
+ install as install_stream_redirect,
75
+ install_from_raw_args as install_stream_redirect_from_raw_args,
76
+ )
77
+
68
78
  __all__ = [
69
79
  "build_json_ok",
70
80
  "build_json_error",
@@ -119,4 +129,11 @@ __all__ = [
119
129
  "SkillInstallReport",
120
130
  "SkillUninstallReport",
121
131
  "run_skill_admin",
132
+ "STDOUT_FILE_ARG",
133
+ "STDERR_FILE_ARG",
134
+ "StreamRedirectConfig",
135
+ "InstalledStreamRedirect",
136
+ "stream_redirect_config_from_raw_args",
137
+ "install_stream_redirect",
138
+ "install_stream_redirect_from_raw_args",
122
139
  ]
@@ -0,0 +1,159 @@
1
+ """Optional stdout/stderr file redirection for AFDATA CLIs."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import sys
7
+ from dataclasses import dataclass
8
+ from typing import Sequence
9
+
10
+ STDOUT_FILE_ARG = "--stdout-file"
11
+ STDERR_FILE_ARG = "--stderr-file"
12
+
13
+
14
+ @dataclass(frozen=True)
15
+ class StreamRedirectConfig:
16
+ """Resolved stdout/stderr file redirection config."""
17
+
18
+ stdout_file: str | None = None
19
+ stderr_file: str | None = None
20
+
21
+ def validate(self) -> None:
22
+ if self.stdout_file is not None and self.stdout_file == "":
23
+ raise ValueError("--stdout-file must not be empty")
24
+ if self.stderr_file is not None and self.stderr_file == "":
25
+ raise ValueError("--stderr-file must not be empty")
26
+
27
+
28
+ class InstalledStreamRedirect:
29
+ """Restores original stdout/stderr when closed."""
30
+
31
+ def __init__(self, stdout_restore_fd: int | None, stderr_restore_fd: int | None) -> None:
32
+ self._stdout_restore_fd = stdout_restore_fd
33
+ self._stderr_restore_fd = stderr_restore_fd
34
+ self._closed = False
35
+
36
+ def close(self) -> None:
37
+ if self._closed:
38
+ return
39
+ self._closed = True
40
+ sys.stdout.flush()
41
+ sys.stderr.flush()
42
+ if self._stdout_restore_fd is not None:
43
+ os.dup2(self._stdout_restore_fd, sys.stdout.fileno())
44
+ os.close(self._stdout_restore_fd)
45
+ self._stdout_restore_fd = None
46
+ if self._stderr_restore_fd is not None:
47
+ os.dup2(self._stderr_restore_fd, sys.stderr.fileno())
48
+ os.close(self._stderr_restore_fd)
49
+ self._stderr_restore_fd = None
50
+
51
+ def __enter__(self) -> "InstalledStreamRedirect":
52
+ return self
53
+
54
+ def __exit__(self, *_: object) -> None:
55
+ self.close()
56
+
57
+
58
+ def config_from_raw_args(args: Sequence[str]) -> StreamRedirectConfig | None:
59
+ """Resolve --stdout-file/--stderr-file from raw CLI args."""
60
+
61
+ stdout_file: str | None = None
62
+ stderr_file: str | None = None
63
+ i = 0
64
+ while i < len(args):
65
+ arg = args[i]
66
+ if arg == "--":
67
+ break
68
+ if arg == STDOUT_FILE_ARG:
69
+ stdout_file, i = _take_value(args, i, STDOUT_FILE_ARG)
70
+ elif arg.startswith(f"{STDOUT_FILE_ARG}="):
71
+ stdout_file = arg.split("=", 1)[1]
72
+ elif arg == STDERR_FILE_ARG:
73
+ stderr_file, i = _take_value(args, i, STDERR_FILE_ARG)
74
+ elif arg.startswith(f"{STDERR_FILE_ARG}="):
75
+ stderr_file = arg.split("=", 1)[1]
76
+ i += 1
77
+
78
+ config = StreamRedirectConfig(stdout_file=stdout_file, stderr_file=stderr_file)
79
+ config.validate()
80
+ if config.stdout_file is None and config.stderr_file is None:
81
+ return None
82
+ return config
83
+
84
+
85
+ def install_from_raw_args(args: Sequence[str] | None = None) -> InstalledStreamRedirect | None:
86
+ """Install stdout/stderr redirection from raw CLI args."""
87
+
88
+ raw = sys.argv[1:] if args is None else args
89
+ config = config_from_raw_args(raw)
90
+ if config is None:
91
+ return None
92
+ return install(config)
93
+
94
+
95
+ def install(config: StreamRedirectConfig) -> InstalledStreamRedirect:
96
+ """Redirect configured streams to append-only files."""
97
+
98
+ config.validate()
99
+ stdout_target = _prepare_target(sys.stdout.fileno(), config.stdout_file)
100
+ try:
101
+ stderr_target = _prepare_target(sys.stderr.fileno(), config.stderr_file)
102
+ except Exception:
103
+ _close_prepared(stdout_target)
104
+ raise
105
+
106
+ sys.stdout.flush()
107
+ sys.stderr.flush()
108
+
109
+ try:
110
+ if stdout_target is not None:
111
+ os.dup2(stdout_target.file_fd, sys.stdout.fileno())
112
+ if stderr_target is not None:
113
+ os.dup2(stderr_target.file_fd, sys.stderr.fileno())
114
+ except Exception:
115
+ if stdout_target is not None:
116
+ os.dup2(stdout_target.restore_fd, sys.stdout.fileno())
117
+ _close_prepared(stdout_target)
118
+ _close_prepared(stderr_target)
119
+ raise
120
+
121
+ stdout_restore_fd = stdout_target.restore_fd if stdout_target is not None else None
122
+ stderr_restore_fd = stderr_target.restore_fd if stderr_target is not None else None
123
+ if stdout_target is not None:
124
+ os.close(stdout_target.file_fd)
125
+ if stderr_target is not None:
126
+ os.close(stderr_target.file_fd)
127
+ return InstalledStreamRedirect(stdout_restore_fd, stderr_restore_fd)
128
+
129
+
130
+ @dataclass
131
+ class _PreparedTarget:
132
+ file_fd: int
133
+ restore_fd: int
134
+
135
+
136
+ def _prepare_target(target_fd: int, path: str | None) -> _PreparedTarget | None:
137
+ if path is None:
138
+ return None
139
+ file_fd = os.open(path, os.O_CREAT | os.O_WRONLY | os.O_APPEND, 0o666)
140
+ try:
141
+ restore_fd = os.dup(target_fd)
142
+ except Exception:
143
+ os.close(file_fd)
144
+ raise
145
+ return _PreparedTarget(file_fd=file_fd, restore_fd=restore_fd)
146
+
147
+
148
+ def _close_prepared(target: _PreparedTarget | None) -> None:
149
+ if target is None:
150
+ return
151
+ os.close(target.file_fd)
152
+ os.close(target.restore_fd)
153
+
154
+
155
+ def _take_value(args: Sequence[str], idx: int, flag: str) -> tuple[str, int]:
156
+ next_idx = idx + 1
157
+ if next_idx >= len(args) or args[next_idx].startswith("--"):
158
+ raise ValueError(f"{flag} requires a value")
159
+ return args[next_idx], next_idx
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agent-first-data
3
- Version: 0.14.2
3
+ Version: 0.15.0
4
4
  Summary: A naming convention that lets AI agents understand your data without being told what it means.
5
5
  License-Expression: MIT
6
6
  Project-URL: Repository, https://github.com/agentfirstkit/agent-first-data
@@ -29,7 +29,7 @@ print(output_json(value))
29
29
  print(output_plain(value))
30
30
  ```
31
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`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, and `cli_handle_version_or_continue`.
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`, `is_valid_rfc3339_date`, `is_valid_rfc3339_time`, `cli_parse_output`, `cli_output`, `build_cli_error`, `build_cli_version`, `cli_handle_version_or_continue`, `stream_redirect_config_from_raw_args`, and `install_stream_redirect_from_raw_args`.
33
33
 
34
34
  Logging is available through `init_logging_json`, `init_logging_plain`, `init_logging_yaml`, `span`, and `get_logger`.
35
35
 
@@ -47,6 +47,7 @@ init_logging_json("INFO", secret_names=("authorization",))
47
47
  - Logging records use `code: "log"` plus a separate `level` field, so error-level logs are not terminal protocol errors.
48
48
  - `build_cli_error(message, hint?)` returns `{code:"error", error: message, hint?}` only.
49
49
  - Use `cli_handle_version_or_continue()` before argument parsing so bare `--version` stays conventional and `--version --output json|yaml|plain` stays structured.
50
+ - Use `install_stream_redirect_from_raw_args()` before version/help handling if a CLI exposes `--stdout-file` or `--stderr-file`; stderr is redirected as native diagnostics, not JSON.
50
51
 
51
52
  ## Reference
52
53
 
@@ -5,6 +5,7 @@ agent_first_data/afdata_logging.py
5
5
  agent_first_data/cli.py
6
6
  agent_first_data/format.py
7
7
  agent_first_data/skill.py
8
+ agent_first_data/stream_redirect.py
8
9
  agent_first_data.egg-info/PKG-INFO
9
10
  agent_first_data.egg-info/SOURCES.txt
10
11
  agent_first_data.egg-info/dependency_links.txt
@@ -13,4 +14,5 @@ tests/test_afdata_logging.py
13
14
  tests/test_cli.py
14
15
  tests/test_format.py
15
16
  tests/test_no_stderr_policy.py
16
- tests/test_skill.py
17
+ tests/test_skill.py
18
+ tests/test_stream_redirect.py
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "agent-first-data"
3
- version = "0.14.2"
3
+ version = "0.15.0"
4
4
  description = "A naming convention that lets AI agents understand your data without being told what it means."
5
5
  license = "MIT"
6
6
  readme = "README.md"
@@ -16,6 +16,8 @@ def test_no_stderr_usage_in_runtime_sources() -> None:
16
16
 
17
17
  violations: list[str] = []
18
18
  for path in files:
19
+ if path.name == "stream_redirect.py":
20
+ continue
19
21
  for lineno, line in enumerate(path.read_text(encoding="utf-8").splitlines(), start=1):
20
22
  if DISALLOWED.search(line):
21
23
  violations.append(f"{path.name}:{lineno}: {line.strip()}")
@@ -0,0 +1,47 @@
1
+ """Tests for stdout/stderr stream redirection argument handling."""
2
+
3
+ import os
4
+ import sys
5
+
6
+ import pytest
7
+
8
+ from agent_first_data.stream_redirect import StreamRedirectConfig, config_from_raw_args, install
9
+
10
+
11
+ def test_config_from_raw_args_space_and_equals_values() -> None:
12
+ config = config_from_raw_args(
13
+ [
14
+ "agent-cli",
15
+ "--stdout-file",
16
+ "/tmp/agent-cli.out",
17
+ "--stderr-file=/tmp/agent-cli.err",
18
+ "ping",
19
+ ]
20
+ )
21
+ assert config is not None
22
+ assert config.stdout_file == "/tmp/agent-cli.out"
23
+ assert config.stderr_file == "/tmp/agent-cli.err"
24
+
25
+
26
+ def test_config_from_raw_args_disabled() -> None:
27
+ assert config_from_raw_args(["agent-cli", "ping"]) is None
28
+
29
+
30
+ def test_config_from_raw_args_missing_value() -> None:
31
+ with pytest.raises(ValueError):
32
+ config_from_raw_args(["agent-cli", "--stderr-file", "--help"])
33
+
34
+
35
+ def test_install_redirects_and_restores_output(tmp_path) -> None:
36
+ stdout_path = tmp_path / "stdout.log"
37
+ stderr_path = tmp_path / "stderr.log"
38
+
39
+ redirect = install(StreamRedirectConfig(stdout_file=str(stdout_path), stderr_file=str(stderr_path)))
40
+ try:
41
+ os.write(sys.stdout.fileno(), b"stdout bytes\n")
42
+ os.write(sys.stderr.fileno(), b"stderr bytes\n")
43
+ finally:
44
+ redirect.close()
45
+
46
+ assert stdout_path.read_bytes() == b"stdout bytes\n"
47
+ assert stderr_path.read_bytes() == b"stderr bytes\n"