chatenv 0.2.0__tar.gz → 0.2.2__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.0/src/chatenv.egg-info → chatenv-0.2.2}/PKG-INFO +6 -3
  2. {chatenv-0.2.0 → chatenv-0.2.2}/README.md +3 -0
  3. {chatenv-0.2.0 → chatenv-0.2.2}/pyproject.toml +2 -2
  4. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/__init__.py +1 -1
  5. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/cli.py +75 -3
  6. {chatenv-0.2.0 → chatenv-0.2.2/src/chatenv.egg-info}/PKG-INFO +6 -3
  7. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv.egg-info/requires.txt +2 -2
  8. {chatenv-0.2.0 → chatenv-0.2.2}/tests/test_chatenv_core.py +78 -1
  9. chatenv-0.2.2/tests/test_version.py +14 -0
  10. chatenv-0.2.0/tests/test_version.py +0 -5
  11. {chatenv-0.2.0 → chatenv-0.2.2}/LICENSE +0 -0
  12. {chatenv-0.2.0 → chatenv-0.2.2}/setup.cfg +0 -0
  13. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/configs.py +0 -0
  14. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/discovery.py +0 -0
  15. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/fields.py +0 -0
  16. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/paste.py +0 -0
  17. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/paths.py +0 -0
  18. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/presets/__init__.py +0 -0
  19. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/registry.py +0 -0
  20. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/source_chain.py +0 -0
  21. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/store.py +0 -0
  22. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv/utils.py +0 -0
  23. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv.egg-info/SOURCES.txt +0 -0
  24. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv.egg-info/dependency_links.txt +0 -0
  25. {chatenv-0.2.0 → chatenv-0.2.2}/src/chatenv.egg-info/entry_points.txt +0 -0
  26. {chatenv-0.2.0 → chatenv-0.2.2}/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.0
3
+ Version: 0.2.2
4
4
  Summary: ChatArch typed environment profile manager
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -11,8 +11,8 @@ Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Requires-Dist: chatstyle<0.2.0,>=0.1.0
14
- Requires-Dist: click>=8.0
15
- Requires-Dist: python-dotenv>=1.0
14
+ Requires-Dist: click<9.0,>=8.4.2
15
+ Requires-Dist: python-dotenv<2.0,>=1.2.2
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: build>=1.0; extra == "dev"
18
18
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -86,7 +86,10 @@ $CHATARCH_HOME/envs/
86
86
  `chatenv` CLI 基于已注册 schema 工作。单独安装且没有项目注册 schema 时,它会提示需要先注册配置类型。业务项目通常会在自己的 CLI 中导入 schema 后复用这些能力。
87
87
 
88
88
  ```bash
89
+ chatenv --version
89
90
  chatenv init -t example
91
+ chatenv status
92
+ chatenv status --detail
90
93
  chatenv cat -t example
91
94
  chatenv cat -t example --no-mask | chatenv paste --stdin --profile work --yes
92
95
  chatenv set EXAMPLE_API_KEY=sk-xxx
@@ -59,7 +59,10 @@ $CHATARCH_HOME/envs/
59
59
  `chatenv` CLI 基于已注册 schema 工作。单独安装且没有项目注册 schema 时,它会提示需要先注册配置类型。业务项目通常会在自己的 CLI 中导入 schema 后复用这些能力。
60
60
 
61
61
  ```bash
62
+ chatenv --version
62
63
  chatenv init -t example
64
+ chatenv status
65
+ chatenv status --detail
63
66
  chatenv cat -t example
64
67
  chatenv cat -t example --no-mask | chatenv paste --stdin --profile work --yes
65
68
  chatenv set EXAMPLE_API_KEY=sk-xxx
@@ -13,8 +13,8 @@ authors = [{name = "rexwzh", email = "1073853456@qq.com"}]
13
13
  keywords = ["chatenv", "chatarch", "env"]
14
14
  dependencies = [
15
15
  "chatstyle>=0.1.0,<0.2.0",
16
- "click>=8.0",
17
- "python-dotenv>=1.0",
16
+ "click>=8.4.2,<9.0",
17
+ "python-dotenv>=1.2.2,<2.0",
18
18
  ]
19
19
  classifiers = [
20
20
  "Programming Language :: Python :: 3",
@@ -20,4 +20,4 @@ __all__ = [
20
20
  "__version__",
21
21
  ]
22
22
 
23
- __version__ = "0.2.0"
23
+ __version__ = "0.2.2"
@@ -19,7 +19,8 @@ from chatstyle import (
19
19
  resolve_interactive_mode as _chatstyle_resolve_interactive_mode,
20
20
  )
21
21
 
22
- from .discovery import load_config_providers
22
+ from . import __version__
23
+ from .discovery import get_provider_errors, load_config_providers
23
24
  from .fields import BaseEnvConfig, EnvField, normalize_profile_name
24
25
  from .paste import iter_fields_for_values, parse_pasted_env_text
25
26
  from .paths import get_paths
@@ -104,6 +105,7 @@ COMMAND_ORDER = [
104
105
  "paste",
105
106
  "use",
106
107
  "list",
108
+ "status",
107
109
  "cat",
108
110
  "get",
109
111
  "set",
@@ -122,6 +124,7 @@ class OrderedGroup(click.Group):
122
124
 
123
125
 
124
126
  @click.group(name="chatenv", cls=OrderedGroup)
127
+ @click.version_option(__version__, prog_name="chatenv")
125
128
  @click.option("--home", type=click.Path(file_okay=False, path_type=Path), help="Override CHATARCH_HOME for this command.")
126
129
  @click.pass_context
127
130
  def cli(ctx: click.Context, home: Path | None):
@@ -187,6 +190,43 @@ def _available_config_types_message() -> str:
187
190
  return "\n".join(lines)
188
191
 
189
192
 
193
+ def _provider_name(config_cls: type[BaseEnvConfig]) -> str:
194
+ return str(getattr(config_cls, "_provider", config_cls.__module__.split(".", 1)[0]))
195
+
196
+
197
+ def _status_config_line(config_cls: type[BaseEnvConfig], store: EnvStore) -> str:
198
+ aliases = ",".join(getattr(config_cls, "_aliases", [])) or "-"
199
+ profiles = store.list_profiles(config_cls)
200
+ active_marker = "yes" if store.active_path(config_cls).exists() else "no"
201
+ return (
202
+ f"- {config_cls.get_storage_name()} | title={config_cls._title} | "
203
+ f"provider={_provider_name(config_cls)} | aliases={aliases} | "
204
+ f"fields={len(config_cls.get_fields())} | active={active_marker} | profiles={len(profiles)}"
205
+ )
206
+
207
+
208
+ def _status_field_line(config_cls: type[BaseEnvConfig], field_name: str, field: EnvField) -> str:
209
+ value_set = field.value not in (None, "")
210
+ default = field.default
211
+ if field.is_sensitive and default not in (None, ""):
212
+ default_text = mask_secret(str(default))
213
+ else:
214
+ default_text = "" if default is None else str(default)
215
+ parts = [
216
+ f" - {field.env_key}",
217
+ f"name={field_name}",
218
+ f"provider={_provider_name(config_cls)}",
219
+ ]
220
+ if field.desc:
221
+ parts.append(f"desc={field.desc}")
222
+ if default_text:
223
+ parts.append(f"default={default_text}")
224
+ if field.is_sensitive:
225
+ parts.append("sensitive=yes")
226
+ parts.append(f"configured={'yes' if value_set else 'no'}")
227
+ return " | ".join(parts)
228
+
229
+
190
230
  def _select_config_interactive(
191
231
  *,
192
232
  action: str,
@@ -341,7 +381,7 @@ def init_env(ctx: click.Context, config_types: tuple[str, ...], interactive: boo
341
381
  @click.option("--type", "config_types", "-t", multiple=True, help="Filter config types.")
342
382
  @click.pass_context
343
383
  def list_env(ctx: click.Context, config_types: tuple[str, ...]):
344
- """List available named profiles grouped by config type."""
384
+ """List active default and named profiles grouped by config type."""
345
385
  store = _store(ctx)
346
386
  configs = _matched_or_all(config_types)
347
387
  _ensure_registered()
@@ -350,16 +390,48 @@ def list_env(ctx: click.Context, config_types: tuple[str, ...]):
350
390
  found = False
351
391
  for config_cls in configs:
352
392
  profiles = store.list_profiles(config_cls)
353
- if not profiles:
393
+ has_default = store.active_path(config_cls).exists()
394
+ if not has_default and not profiles:
354
395
  continue
355
396
  found = True
356
397
  click.echo(f"[{config_cls.get_storage_name()}]")
398
+ if has_default:
399
+ click.echo("- .env [default]")
357
400
  for profile in profiles:
358
401
  click.echo(f"- {profile}.env")
359
402
  if not found:
360
403
  click.echo(f"No profiles found under {_envs_dir(ctx)}")
361
404
 
362
405
 
406
+ @cli.command(name="status")
407
+ @click.option("--type", "config_types", "-t", multiple=True, help="Filter config types.")
408
+ @click.option("--detail", is_flag=True, help="Show each registered variable and its provider package.")
409
+ @click.pass_context
410
+ def status_env(ctx: click.Context, config_types: tuple[str, ...], detail: bool):
411
+ """Show registered config platforms and provider ownership."""
412
+ store = _store(ctx)
413
+ configs = _matched_or_all(config_types)
414
+ _ensure_registered()
415
+ if config_types and not configs:
416
+ raise click.ClickException(f"No configuration types matched: {', '.join(config_types)}")
417
+ _load_all(ctx)
418
+ click.echo(f"Registered config platforms: {len(configs)}")
419
+ for index, config_cls in enumerate(configs):
420
+ if index:
421
+ click.echo("")
422
+ click.echo(_status_config_line(config_cls, store))
423
+ if not detail:
424
+ continue
425
+ for field_name, field in config_cls.get_fields().items():
426
+ click.echo(_status_field_line(config_cls, field_name, field))
427
+ errors = get_provider_errors()
428
+ if errors:
429
+ click.echo("")
430
+ click.echo(f"Provider load errors: {len(errors)}")
431
+ for provider, exc in errors.items():
432
+ click.echo(f"- {provider}: {exc}")
433
+
434
+
363
435
  @cli.command(name="cat")
364
436
  @click.argument("name", required=False)
365
437
  @click.option("--no-mask", is_flag=True, help="Show sensitive values in plain text.")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: chatenv
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: ChatArch typed environment profile manager
5
5
  Author-email: rexwzh <1073853456@qq.com>
6
6
  License-Expression: MIT
@@ -11,8 +11,8 @@ Requires-Python: >=3.10
11
11
  Description-Content-Type: text/markdown
12
12
  License-File: LICENSE
13
13
  Requires-Dist: chatstyle<0.2.0,>=0.1.0
14
- Requires-Dist: click>=8.0
15
- Requires-Dist: python-dotenv>=1.0
14
+ Requires-Dist: click<9.0,>=8.4.2
15
+ Requires-Dist: python-dotenv<2.0,>=1.2.2
16
16
  Provides-Extra: dev
17
17
  Requires-Dist: build>=1.0; extra == "dev"
18
18
  Requires-Dist: pytest>=8.0; extra == "dev"
@@ -86,7 +86,10 @@ $CHATARCH_HOME/envs/
86
86
  `chatenv` CLI 基于已注册 schema 工作。单独安装且没有项目注册 schema 时,它会提示需要先注册配置类型。业务项目通常会在自己的 CLI 中导入 schema 后复用这些能力。
87
87
 
88
88
  ```bash
89
+ chatenv --version
89
90
  chatenv init -t example
91
+ chatenv status
92
+ chatenv status --detail
90
93
  chatenv cat -t example
91
94
  chatenv cat -t example --no-mask | chatenv paste --stdin --profile work --yes
92
95
  chatenv set EXAMPLE_API_KEY=sk-xxx
@@ -1,6 +1,6 @@
1
1
  chatstyle<0.2.0,>=0.1.0
2
- click>=8.0
3
- python-dotenv>=1.0
2
+ click<9.0,>=8.4.2
3
+ python-dotenv<2.0,>=1.2.2
4
4
 
5
5
  [dev]
6
6
  build>=1.0
@@ -92,13 +92,48 @@ def test_duplicate_provider_config_does_not_pollute_registry(monkeypatch):
92
92
  discovery_module._provider_configs.update(provider_configs_before)
93
93
 
94
94
 
95
- def test_chatarch_internal_dependencies_have_upper_bounds():
95
+ def test_runtime_dependencies_use_reviewed_latest_windows():
96
96
  pyproject_text = (Path(__file__).resolve().parents[1] / "pyproject.toml").read_text(
97
97
  encoding="utf-8"
98
98
  )
99
99
 
100
100
  assert '"chatstyle>=0.1.0,<0.2.0"' in pyproject_text
101
+ assert '"click>=8.4.2,<9.0"' in pyproject_text
102
+ assert '"python-dotenv>=1.2.2,<2.0"' in pyproject_text
101
103
  assert '"chatstyle>=0.1.0"' not in pyproject_text
104
+ assert '"click>=8.0"' not in pyproject_text
105
+ assert '"python-dotenv>=1.0"' not in pyproject_text
106
+
107
+
108
+ def test_status_lists_registered_platforms_and_provider_detail(tmp_path):
109
+ runner = CliRunner()
110
+ home = tmp_path / "arch"
111
+ result = runner.invoke(cli, ["--home", str(home), "status", "--type", "unit", "--detail"])
112
+
113
+ assert result.exit_code == 0, result.output
114
+ assert "Registered config platforms: 1" in result.output
115
+ assert "Unit | title=Unit Configuration" in result.output
116
+ assert "provider=" in result.output
117
+ assert "UNIT_KEY" in result.output
118
+ assert "sensitive=yes" in result.output
119
+ assert "UNIT_VALUE" in result.output
120
+ assert "default=default" in result.output
121
+
122
+
123
+ def test_status_reports_provider_name_from_loaded_package(tmp_path):
124
+ previous = getattr(UnitConfig, "_provider", None)
125
+ setattr(UnitConfig, "_provider", "chatunit")
126
+ try:
127
+ result = CliRunner().invoke(cli, ["--home", str(tmp_path / "arch"), "status", "--type", "unit", "--detail"])
128
+ finally:
129
+ if previous is None:
130
+ delattr(UnitConfig, "_provider")
131
+ else:
132
+ setattr(UnitConfig, "_provider", previous)
133
+
134
+ assert result.exit_code == 0, result.output
135
+ assert "provider=chatunit" in result.output
136
+ assert "UNIT_KEY | name=UNIT_KEY | provider=chatunit" in result.output
102
137
 
103
138
 
104
139
  def test_cli_set_only_writes_the_requested_key(monkeypatch, tmp_path):
@@ -144,6 +179,47 @@ def test_cli_paste_active_merges_file_values_without_system_env(monkeypatch, tmp
144
179
  assert "OPENAI_API_KEY" not in env_text
145
180
 
146
181
 
182
+
183
+
184
+ def test_list_marks_active_default_profile(tmp_path):
185
+ runner = CliRunner()
186
+ home = tmp_path / "arch"
187
+ store = EnvStore(home / "envs")
188
+ store.save_active(UnitConfig, {"UNIT_VALUE": "active"})
189
+ store.save_profile(UnitConfig, "work", {"UNIT_VALUE": "named"})
190
+
191
+ result = runner.invoke(cli, ["--home", str(home), "list", "--type", "unit"])
192
+
193
+ assert result.exit_code == 0, result.output
194
+ assert "[Unit]" in result.output
195
+ assert "- .env [default]" in result.output
196
+ assert "- work.env" in result.output
197
+
198
+
199
+
200
+ def test_list_keeps_named_only_profiles_without_default_marker(tmp_path):
201
+ runner = CliRunner()
202
+ home = tmp_path / "arch"
203
+ store = EnvStore(home / "envs")
204
+ store.save_profile(UnitConfig, "work", {"UNIT_VALUE": "named"})
205
+
206
+ result = runner.invoke(cli, ["--home", str(home), "list", "--type", "unit"])
207
+
208
+ assert result.exit_code == 0, result.output
209
+ assert "[Unit]" in result.output
210
+ assert "- .env [default]" not in result.output
211
+ assert "- work.env" in result.output
212
+
213
+ def test_list_reports_no_profiles_when_active_and_named_profiles_absent(tmp_path):
214
+ runner = CliRunner()
215
+ home = tmp_path / "arch"
216
+ (home / "envs" / "Unit").mkdir(parents=True)
217
+
218
+ result = runner.invoke(cli, ["--home", str(home), "list", "--type", "unit"])
219
+
220
+ assert result.exit_code == 0, result.output
221
+ assert "No profiles found under" in result.output
222
+
147
223
  def test_paths_only_use_chatarch_home(monkeypatch, tmp_path):
148
224
  monkeypatch.setenv("CHATARCH_HOME", str(tmp_path / "arch"))
149
225
  paths = get_paths()
@@ -548,6 +624,7 @@ def test_cli_help_uses_stable_command_order():
548
624
  "paste",
549
625
  "use",
550
626
  "list",
627
+ "status",
551
628
  "cat",
552
629
  "get",
553
630
  "set",
@@ -0,0 +1,14 @@
1
+ from chatenv import __version__
2
+ from chatenv.cli import cli
3
+ from click.testing import CliRunner
4
+
5
+
6
+ def test_version_present():
7
+ assert __version__ == "0.2.2"
8
+
9
+
10
+ def test_cli_version_option():
11
+ result = CliRunner().invoke(cli, ["--version"])
12
+
13
+ assert result.exit_code == 0
14
+ assert __version__ in result.output
@@ -1,5 +0,0 @@
1
- from chatenv import __version__
2
-
3
-
4
- def test_version_present():
5
- assert __version__ == "0.2.0"
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