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.
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/PKG-INFO +3 -2
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/README.md +2 -1
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/__init__.py +17 -0
- agent_first_data-0.15.0/agent_first_data/stream_redirect.py +159 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/PKG-INFO +3 -2
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/SOURCES.txt +3 -1
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/pyproject.toml +1 -1
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_no_stderr_policy.py +2 -0
- agent_first_data-0.15.0/tests/test_stream_redirect.py +47 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/afdata_logging.py +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/cli.py +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/format.py +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data/skill.py +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/dependency_links.txt +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/top_level.txt +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/setup.cfg +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_afdata_logging.py +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_cli.py +0 -0
- {agent_first_data-0.14.2 → agent_first_data-0.15.0}/tests/test_format.py +0 -0
- {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.
|
|
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 `
|
|
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 `
|
|
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.
|
|
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 `
|
|
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
|
|
@@ -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"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{agent_first_data-0.14.2 → agent_first_data-0.15.0}/agent_first_data.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|