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.
Files changed (26) hide show
  1. {chatenv-0.2.5/src/chatenv.egg-info → chatenv-0.2.6}/PKG-INFO +1 -1
  2. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/__init__.py +1 -1
  3. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/cli.py +20 -11
  4. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/tokens.py +21 -1
  5. {chatenv-0.2.5 → chatenv-0.2.6/src/chatenv.egg-info}/PKG-INFO +1 -1
  6. {chatenv-0.2.5 → chatenv-0.2.6}/tests/test_chatenv_core.py +43 -0
  7. {chatenv-0.2.5 → chatenv-0.2.6}/tests/test_version.py +1 -1
  8. {chatenv-0.2.5 → chatenv-0.2.6}/LICENSE +0 -0
  9. {chatenv-0.2.5 → chatenv-0.2.6}/README.md +0 -0
  10. {chatenv-0.2.5 → chatenv-0.2.6}/pyproject.toml +0 -0
  11. {chatenv-0.2.5 → chatenv-0.2.6}/setup.cfg +0 -0
  12. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/configs.py +0 -0
  13. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/discovery.py +0 -0
  14. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/fields.py +0 -0
  15. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/paste.py +0 -0
  16. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/paths.py +0 -0
  17. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/presets/__init__.py +0 -0
  18. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/registry.py +0 -0
  19. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/source_chain.py +0 -0
  20. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/store.py +0 -0
  21. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv/utils.py +0 -0
  22. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/SOURCES.txt +0 -0
  23. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/dependency_links.txt +0 -0
  24. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/entry_points.txt +0 -0
  25. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/requires.txt +0 -0
  26. {chatenv-0.2.5 → chatenv-0.2.6}/src/chatenv.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatenv
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: ChatArch typed environment profile manager
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -22,4 +22,4 @@ __all__ = [
22
22
  "__version__",
23
23
  ]
24
24
 
25
- __version__ = "0.2.5"
25
+ __version__ = "0.2.6"
@@ -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
- payload = _token_store(ctx).write(
663
- service,
664
- profile,
665
- values=values,
666
- token_type=token_type,
667
- summary=_parse_summary(summary),
668
- expires_at=expires_at,
669
- source="refresh",
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
- payload = _token_store(ctx).status(service, profile)
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
- payload = _token_store(ctx).clear(service, profile, execute=execute)
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 _safe_segment(value or DEFAULT_PROFILE, default=DEFAULT_PROFILE)
64
+ return _strict_token_profile_segment(value)
45
65
 
46
66
 
47
67
  def _coerce_mapping(value: dict[str, Any] | None) -> dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatenv
3
- Version: 0.2.5
3
+ Version: 0.2.6
4
4
  Summary: ChatArch typed environment profile manager
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -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"})
@@ -4,7 +4,7 @@ from click.testing import CliRunner
4
4
 
5
5
 
6
6
  def test_version_present():
7
- assert __version__ == "0.2.5"
7
+ assert __version__ == "0.2.6"
8
8
 
9
9
 
10
10
  def test_cli_version_option():
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