chatenv 0.2.5__tar.gz → 0.2.6__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.
- {chatenv-0.2.5/src/chatenv.egg-info → chatenv-0.2.6}/PKG-INFO +1 -1
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/__init__.py +1 -1
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/cli.py +20 -11
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/tokens.py +21 -1
- {chatenv-0.2.5 → chatenv-0.2.6/src/chatenv.egg-info}/PKG-INFO +1 -1
- {chatenv-0.2.5 → chatenv-0.2.6}/tests/test_chatenv_core.py +43 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/tests/test_version.py +1 -1
- {chatenv-0.2.5 → chatenv-0.2.6}/LICENSE +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/README.md +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/pyproject.toml +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/setup.cfg +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/configs.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/discovery.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/fields.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/paste.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/paths.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/presets/__init__.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/registry.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/source_chain.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/store.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/utils.py +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/SOURCES.txt +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/dependency_links.txt +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/entry_points.txt +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/requires.txt +0 -0
- {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/top_level.txt +0 -0
|
@@ -659,15 +659,18 @@ def token_refresh(
|
|
|
659
659
|
):
|
|
660
660
|
"""Write refreshed generic runtime token JSON for SERVICE/PROFILE."""
|
|
661
661
|
values = _read_token_values(read_stdin=read_stdin, value_file=value_file)
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
662
|
+
try:
|
|
663
|
+
payload = _token_store(ctx).write(
|
|
664
|
+
service,
|
|
665
|
+
profile,
|
|
666
|
+
values=values,
|
|
667
|
+
token_type=token_type,
|
|
668
|
+
summary=_parse_summary(summary),
|
|
669
|
+
expires_at=expires_at,
|
|
670
|
+
source="refresh",
|
|
671
|
+
)
|
|
672
|
+
except ValueError as exc:
|
|
673
|
+
raise click.ClickException(str(exc)) from exc
|
|
671
674
|
if output_format == "json":
|
|
672
675
|
_echo_json(payload)
|
|
673
676
|
else:
|
|
@@ -681,7 +684,10 @@ def token_refresh(
|
|
|
681
684
|
@click.pass_context
|
|
682
685
|
def token_status(ctx: click.Context, service: str, profile: str, output_format: str):
|
|
683
686
|
"""Show safe runtime token metadata for SERVICE/PROFILE."""
|
|
684
|
-
|
|
687
|
+
try:
|
|
688
|
+
payload = _token_store(ctx).status(service, profile)
|
|
689
|
+
except ValueError as exc:
|
|
690
|
+
raise click.ClickException(str(exc)) from exc
|
|
685
691
|
if output_format == "json":
|
|
686
692
|
_echo_json(payload)
|
|
687
693
|
else:
|
|
@@ -721,7 +727,10 @@ def token_list(ctx: click.Context, service: str | None, output_format: str):
|
|
|
721
727
|
@click.pass_context
|
|
722
728
|
def token_clear(ctx: click.Context, service: str, profile: str, execute: bool, output_format: str):
|
|
723
729
|
"""Clear a generic runtime token file for SERVICE/PROFILE."""
|
|
724
|
-
|
|
730
|
+
try:
|
|
731
|
+
payload = _token_store(ctx).clear(service, profile, execute=execute)
|
|
732
|
+
except ValueError as exc:
|
|
733
|
+
raise click.ClickException(str(exc)) from exc
|
|
725
734
|
if output_format == "json":
|
|
726
735
|
_echo_json(payload)
|
|
727
736
|
else:
|
|
@@ -36,12 +36,32 @@ def _safe_segment(value: str, *, default: str) -> str:
|
|
|
36
36
|
return cleaned or default
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
def _strict_token_profile_segment(value: str | None) -> str:
|
|
40
|
+
if value is None:
|
|
41
|
+
return DEFAULT_PROFILE
|
|
42
|
+
raw = str(value)
|
|
43
|
+
stripped = raw.strip()
|
|
44
|
+
if not stripped:
|
|
45
|
+
raise ValueError("token profile must not be empty")
|
|
46
|
+
if stripped != raw:
|
|
47
|
+
raise ValueError("token profile must not contain leading or trailing whitespace")
|
|
48
|
+
if stripped in {".", ".."}:
|
|
49
|
+
raise ValueError("token profile must not be '.' or '..'")
|
|
50
|
+
if "/" in stripped or "\\" in stripped:
|
|
51
|
+
raise ValueError("token profile must be a single path segment")
|
|
52
|
+
if re.search(r"[^A-Za-z0-9_.-]", stripped):
|
|
53
|
+
raise ValueError("token profile may contain only letters, numbers, dot, underscore, and hyphen")
|
|
54
|
+
if stripped.strip(".-") != stripped:
|
|
55
|
+
raise ValueError("token profile must not start or end with dot or hyphen")
|
|
56
|
+
return stripped
|
|
57
|
+
|
|
58
|
+
|
|
39
59
|
def normalize_service_name(value: str) -> str:
|
|
40
60
|
return _safe_segment(value, default="Service")
|
|
41
61
|
|
|
42
62
|
|
|
43
63
|
def normalize_token_profile(value: str | None) -> str:
|
|
44
|
-
return
|
|
64
|
+
return _strict_token_profile_segment(value)
|
|
45
65
|
|
|
46
66
|
|
|
47
67
|
def _coerce_mapping(value: dict[str, Any] | None) -> dict[str, Any]:
|
|
@@ -2,6 +2,7 @@ import json
|
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
from types import SimpleNamespace
|
|
4
4
|
|
|
5
|
+
import pytest
|
|
5
6
|
from click.testing import CliRunner
|
|
6
7
|
import chatenv.discovery as discovery_module
|
|
7
8
|
import chatenv.cli as cli_module
|
|
@@ -294,6 +295,48 @@ def test_token_store_list_and_clear_are_generic(tmp_path):
|
|
|
294
295
|
assert store.status("PyPI", "RexWzh")["token_present"] is False
|
|
295
296
|
|
|
296
297
|
|
|
298
|
+
def test_token_store_rejects_aliasing_profile_segments(tmp_path):
|
|
299
|
+
from chatenv.tokens import TokenStore, normalize_token_profile
|
|
300
|
+
|
|
301
|
+
store = TokenStore(home=tmp_path / "arch")
|
|
302
|
+
store.write("PyPI", "RexWzh", values={"session": "opaque-value"}, token_type="web_session")
|
|
303
|
+
|
|
304
|
+
invalid_profiles = [
|
|
305
|
+
"",
|
|
306
|
+
" ",
|
|
307
|
+
".",
|
|
308
|
+
"..",
|
|
309
|
+
"../RexWzh",
|
|
310
|
+
"RexWzh/other",
|
|
311
|
+
"RexWzh\\other",
|
|
312
|
+
" RexWzh",
|
|
313
|
+
"RexWzh ",
|
|
314
|
+
".RexWzh",
|
|
315
|
+
"RexWzh.",
|
|
316
|
+
"Rex Wzh",
|
|
317
|
+
]
|
|
318
|
+
for profile in invalid_profiles:
|
|
319
|
+
with pytest.raises(ValueError):
|
|
320
|
+
normalize_token_profile(profile)
|
|
321
|
+
with pytest.raises(ValueError):
|
|
322
|
+
store.status("PyPI", profile)
|
|
323
|
+
|
|
324
|
+
assert store.status("PyPI", "RexWzh")["token_present"] is True
|
|
325
|
+
assert store.status("PyPI", None)["profile"] == "default"
|
|
326
|
+
|
|
327
|
+
|
|
328
|
+
def test_token_cli_reports_invalid_profile_without_aliasing(tmp_path):
|
|
329
|
+
runner = CliRunner()
|
|
330
|
+
result = runner.invoke(
|
|
331
|
+
cli,
|
|
332
|
+
["--home", str(tmp_path / "arch"), "token", "status", "PyPI", "../RexWzh", "--format", "json"],
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
assert result.exit_code != 0
|
|
336
|
+
assert "single path segment" in result.output
|
|
337
|
+
assert "Traceback" not in result.output
|
|
338
|
+
|
|
339
|
+
|
|
297
340
|
def test_store_profile_roundtrip(tmp_path):
|
|
298
341
|
store = EnvStore(tmp_path / "envs")
|
|
299
342
|
UnitConfig.load_from_sources(env_values={"UNIT_KEY": "secret", "UNIT_VALUE": "x"})
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|