chatenv 0.2.6__tar.gz → 0.2.8__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.8/PKG-INFO +160 -0
- chatenv-0.2.8/README.md +129 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/pyproject.toml +4 -3
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/__init__.py +3 -1
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/cli.py +29 -5
- chatenv-0.2.8/src/chatenv/token_refreshers.py +146 -0
- chatenv-0.2.8/src/chatenv.egg-info/PKG-INFO +160 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv.egg-info/SOURCES.txt +4 -1
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv.egg-info/requires.txt +2 -1
- {chatenv-0.2.6 → chatenv-0.2.8}/tests/test_chatenv_core.py +108 -8
- chatenv-0.2.8/tests/test_docs_contract.py +66 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/tests/test_version.py +3 -2
- chatenv-0.2.8/tests/test_workflow_contract.py +46 -0
- chatenv-0.2.6/PKG-INFO +0 -182
- chatenv-0.2.6/README.md +0 -152
- chatenv-0.2.6/src/chatenv.egg-info/PKG-INFO +0 -182
- {chatenv-0.2.6 → chatenv-0.2.8}/LICENSE +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/setup.cfg +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/configs.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/discovery.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/fields.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/paste.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/paths.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/presets/__init__.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/registry.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/source_chain.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/store.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/tokens.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv/utils.py +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv.egg-info/dependency_links.txt +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv.egg-info/entry_points.txt +0 -0
- {chatenv-0.2.6 → chatenv-0.2.8}/src/chatenv.egg-info/top_level.txt +0 -0
chatenv-0.2.8/PKG-INFO
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chatenv
|
|
3
|
+
Version: 0.2.8
|
|
4
|
+
Summary: ChatArch typed environment profile manager
|
|
5
|
+
Author-email: rexwzh <1073853456@qq.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://arch.gh.wzhecnu.cn/ChatEnv/
|
|
8
|
+
Project-URL: Documentation, https://arch.gh.wzhecnu.cn/ChatEnv/
|
|
9
|
+
Project-URL: Repository, https://github.com/ChatArch/ChatEnv
|
|
10
|
+
Keywords: chatenv,chatarch,env
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Requires-Python: >=3.10
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
License-File: LICENSE
|
|
16
|
+
Requires-Dist: chatstyle<0.2.0,>=0.1.0
|
|
17
|
+
Requires-Dist: click<9.0,>=8.4.2
|
|
18
|
+
Requires-Dist: python-dotenv<2.0,>=1.2.2
|
|
19
|
+
Provides-Extra: dev
|
|
20
|
+
Requires-Dist: build>=1.0; extra == "dev"
|
|
21
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
22
|
+
Requires-Dist: twine>=5.0; extra == "dev"
|
|
23
|
+
Provides-Extra: docs
|
|
24
|
+
Requires-Dist: mkdocs<2.0,>=1.6; extra == "docs"
|
|
25
|
+
Requires-Dist: mkdocs-material<10.0,>=9.5; extra == "docs"
|
|
26
|
+
Requires-Dist: mkdocs-static-i18n<2.0,>=1.2; extra == "docs"
|
|
27
|
+
Requires-Dist: mkdocs-minify-plugin<1.0,>=0.8; extra == "docs"
|
|
28
|
+
Requires-Dist: mkdocs-redirects<2.0,>=1.2; extra == "docs"
|
|
29
|
+
Requires-Dist: mike<3.0,>=2.1; extra == "docs"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
<div align="center">
|
|
33
|
+
<a href="https://pypi.python.org/pypi/chatenv">
|
|
34
|
+
<img src="https://img.shields.io/pypi/v/chatenv.svg" alt="PyPI version" />
|
|
35
|
+
</a>
|
|
36
|
+
<a href="https://github.com/ChatArch/ChatEnv/actions/workflows/ci.yml">
|
|
37
|
+
<img src="https://github.com/ChatArch/ChatEnv/actions/workflows/ci.yml/badge.svg" alt="Tests" />
|
|
38
|
+
</a>
|
|
39
|
+
<a href="https://pypi.python.org/pypi/chatenv">
|
|
40
|
+
<img src="https://img.shields.io/pypi/pyversions/chatenv.svg" alt="Python versions" />
|
|
41
|
+
</a>
|
|
42
|
+
<a href="LICENSE">
|
|
43
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" />
|
|
44
|
+
</a>
|
|
45
|
+
</div>
|
|
46
|
+
|
|
47
|
+
<div align="center">
|
|
48
|
+
|
|
49
|
+
# ChatEnv
|
|
50
|
+
|
|
51
|
+
ChatArch typed env/profile runtime.
|
|
52
|
+
|
|
53
|
+
</div>
|
|
54
|
+
|
|
55
|
+
ChatEnv 是 ChatArch / chatxxx 系列项目共用的 typed env/profile 底层包。它提供字段描述、配置基类、registry、路径、profile 文件读写、mask、paste 解析,以及 runtime token-store 的通用能力;具体业务变量、登录刷新和连通性语义由各项目自己定义并注册。
|
|
56
|
+
|
|
57
|
+
文档入口:https://arch.gh.wzhecnu.cn/ChatEnv/
|
|
58
|
+
|
|
59
|
+
## 安装
|
|
60
|
+
|
|
61
|
+
```bash
|
|
62
|
+
pip install chatenv --upgrade
|
|
63
|
+
chatenv --version
|
|
64
|
+
chatenv --tree
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
支持 Python `>=3.10`。
|
|
68
|
+
|
|
69
|
+
## 目录
|
|
70
|
+
|
|
71
|
+
```text
|
|
72
|
+
CHATARCH_HOME=${CHATARCH_HOME:-~/.chatarch}
|
|
73
|
+
$CHATARCH_HOME/envs/ # stable typed env/profile files
|
|
74
|
+
$CHATARCH_HOME/tokens/ # generated runtime tokens/sessions, parallel to env profiles
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
ChatEnv 只负责 stable env/profile 与 runtime token-store 的存储规则,不额外创建 config/cache/data/state,也不把 `tokens/` 当作第二个手工维护 secret/env 层。
|
|
78
|
+
|
|
79
|
+
## CLI 树
|
|
80
|
+
|
|
81
|
+
`chatenv --tree` 从当前安装包的 Click 注册表实时输出完整命令面:
|
|
82
|
+
|
|
83
|
+
```text
|
|
84
|
+
chatenv [--home <HOME>] # Manage typed env profiles under $CHATARCH_HOME/envs.
|
|
85
|
+
├── --help # Show this help message.
|
|
86
|
+
├── --version # Show the installed package version.
|
|
87
|
+
├── --tree # Print the registered command tree.
|
|
88
|
+
├── init [--type <CONFIG-TYPES>] [--interactive/--no-interactive] # Create or update active typed env files.
|
|
89
|
+
├── new [NAME] [--type <CONFIG-TYPES>] [--yes] [--interactive/--no-interactive] # Create a named typed profile without activating it.
|
|
90
|
+
├── paste [--value <VALUE>] [--stdin] [--profile <PROFILE>] [--yes] [--interactive/--no-interactive] # Paste loose env text and import recognized keys.
|
|
91
|
+
├── use [NAME] [--type <CONFIG-TYPES>] [--interactive/--no-interactive] # Activate a named profile for one config type.
|
|
92
|
+
├── list [--type <CONFIG-TYPES>] # List active default and named profiles grouped by config type.
|
|
93
|
+
├── status [--type <CONFIG-TYPES>] [--detail] # Show registered config platforms and provider ownership.
|
|
94
|
+
├── token # Manage generic runtime token profiles.
|
|
95
|
+
│ ├── status <SERVICE> [PROFILE] [--format <OUTPUT-FORMAT>] # Show safe runtime token metadata for SERVICE/PROFILE.
|
|
96
|
+
│ ├── refresh <SERVICE> [PROFILE] [--format <OUTPUT-FORMAT>] # Refresh SERVICE/PROFILE through a registered service refresh provider.
|
|
97
|
+
│ ├── import <SERVICE> [PROFILE] [--stdin] [--file <VALUE-FILE>] [--token-type <TOKEN-TYPE>] [--summary <SUMMARY>] [--expires-at <EXPIRES-AT>] [--format <OUTPUT-FORMAT>] # Explicitly import externally refreshed runtime token JSON.
|
|
98
|
+
│ ├── list [SERVICE] [--format <OUTPUT-FORMAT>] # List runtime token profiles grouped by service.
|
|
99
|
+
│ └── clear <SERVICE> [PROFILE] [--execute] [--format <OUTPUT-FORMAT>] # Clear a generic runtime token file for SERVICE/PROFILE.
|
|
100
|
+
├── cat [NAME] [--no-mask] [--type <CONFIG-TYPES>] # Print active values, or a named typed profile with -t TYPE NAME.
|
|
101
|
+
├── get [KEY] [--interactive/--no-interactive] # Get a configuration value from active typed env files.
|
|
102
|
+
├── set [KEY-VALUE] [--interactive/--no-interactive] # Set a configuration value in the matching active typed env file.
|
|
103
|
+
├── save [NAME] [--type <CONFIG-TYPES>] [--yes] [--interactive/--no-interactive] # Save current active values as a named profile.
|
|
104
|
+
├── delete [NAME] [--type <CONFIG-TYPES>] [--yes] [--interactive/--no-interactive] # Delete a named profile for one config type.
|
|
105
|
+
└── test [--target <TARGET>] [--interactive/--no-interactive] # Test a registered configuration schema.
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## 常用命令
|
|
109
|
+
|
|
110
|
+
```bash
|
|
111
|
+
chatenv init -t example
|
|
112
|
+
chatenv status --detail
|
|
113
|
+
chatenv cat -t example
|
|
114
|
+
chatenv paste --stdin --profile work --yes
|
|
115
|
+
chatenv set EXAMPLE_API_KEY=sk-xxx
|
|
116
|
+
chatenv get EXAMPLE_API_KEY
|
|
117
|
+
chatenv token refresh PyPI RexWzh
|
|
118
|
+
chatenv token status PyPI RexWzh
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
敏感值默认 mask;`token status/list/clear` 只输出 safe metadata,不输出 raw token/cookie/CSRF values。
|
|
122
|
+
|
|
123
|
+
## Python API
|
|
124
|
+
|
|
125
|
+
```python
|
|
126
|
+
from chatenv import BaseEnvConfig, EnvField, EnvStore, get_paths
|
|
127
|
+
|
|
128
|
+
class ExampleConfig(BaseEnvConfig):
|
|
129
|
+
_title = "Example Configuration"
|
|
130
|
+
_aliases = ["example"]
|
|
131
|
+
_storage_dir = "Example"
|
|
132
|
+
|
|
133
|
+
EXAMPLE_API_KEY = EnvField("EXAMPLE_API_KEY", is_sensitive=True)
|
|
134
|
+
|
|
135
|
+
paths = get_paths()
|
|
136
|
+
store = EnvStore(paths.envs_dir)
|
|
137
|
+
store.save_active(ExampleConfig, {"EXAMPLE_API_KEY": "sk-..."})
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## 文档
|
|
141
|
+
|
|
142
|
+
- https://arch.gh.wzhecnu.cn/ChatEnv/
|
|
143
|
+
- `docs/cli.md`:CLI 用法
|
|
144
|
+
- `docs/design.md`:路径、数据布局与注册策略
|
|
145
|
+
- `docs/developer-guide.md`:chatxxx 项目接入和 provider 开发指南
|
|
146
|
+
- `docs/development.md`:测试、构建与发布
|
|
147
|
+
|
|
148
|
+
## 开发
|
|
149
|
+
|
|
150
|
+
```bash
|
|
151
|
+
python -m pip install -e .[dev,docs]
|
|
152
|
+
python -m pytest -q
|
|
153
|
+
python -m mkdocs build --strict
|
|
154
|
+
python -m build
|
|
155
|
+
python -m twine check dist/*
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
## 开源协议
|
|
159
|
+
|
|
160
|
+
MIT License
|
chatenv-0.2.8/README.md
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
<a href="https://pypi.python.org/pypi/chatenv">
|
|
3
|
+
<img src="https://img.shields.io/pypi/v/chatenv.svg" alt="PyPI version" />
|
|
4
|
+
</a>
|
|
5
|
+
<a href="https://github.com/ChatArch/ChatEnv/actions/workflows/ci.yml">
|
|
6
|
+
<img src="https://github.com/ChatArch/ChatEnv/actions/workflows/ci.yml/badge.svg" alt="Tests" />
|
|
7
|
+
</a>
|
|
8
|
+
<a href="https://pypi.python.org/pypi/chatenv">
|
|
9
|
+
<img src="https://img.shields.io/pypi/pyversions/chatenv.svg" alt="Python versions" />
|
|
10
|
+
</a>
|
|
11
|
+
<a href="LICENSE">
|
|
12
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License" />
|
|
13
|
+
</a>
|
|
14
|
+
</div>
|
|
15
|
+
|
|
16
|
+
<div align="center">
|
|
17
|
+
|
|
18
|
+
# ChatEnv
|
|
19
|
+
|
|
20
|
+
ChatArch typed env/profile runtime.
|
|
21
|
+
|
|
22
|
+
</div>
|
|
23
|
+
|
|
24
|
+
ChatEnv 是 ChatArch / chatxxx 系列项目共用的 typed env/profile 底层包。它提供字段描述、配置基类、registry、路径、profile 文件读写、mask、paste 解析,以及 runtime token-store 的通用能力;具体业务变量、登录刷新和连通性语义由各项目自己定义并注册。
|
|
25
|
+
|
|
26
|
+
文档入口:https://arch.gh.wzhecnu.cn/ChatEnv/
|
|
27
|
+
|
|
28
|
+
## 安装
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
pip install chatenv --upgrade
|
|
32
|
+
chatenv --version
|
|
33
|
+
chatenv --tree
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
支持 Python `>=3.10`。
|
|
37
|
+
|
|
38
|
+
## 目录
|
|
39
|
+
|
|
40
|
+
```text
|
|
41
|
+
CHATARCH_HOME=${CHATARCH_HOME:-~/.chatarch}
|
|
42
|
+
$CHATARCH_HOME/envs/ # stable typed env/profile files
|
|
43
|
+
$CHATARCH_HOME/tokens/ # generated runtime tokens/sessions, parallel to env profiles
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
ChatEnv 只负责 stable env/profile 与 runtime token-store 的存储规则,不额外创建 config/cache/data/state,也不把 `tokens/` 当作第二个手工维护 secret/env 层。
|
|
47
|
+
|
|
48
|
+
## CLI 树
|
|
49
|
+
|
|
50
|
+
`chatenv --tree` 从当前安装包的 Click 注册表实时输出完整命令面:
|
|
51
|
+
|
|
52
|
+
```text
|
|
53
|
+
chatenv [--home <HOME>] # Manage typed env profiles under $CHATARCH_HOME/envs.
|
|
54
|
+
├── --help # Show this help message.
|
|
55
|
+
├── --version # Show the installed package version.
|
|
56
|
+
├── --tree # Print the registered command tree.
|
|
57
|
+
├── init [--type <CONFIG-TYPES>] [--interactive/--no-interactive] # Create or update active typed env files.
|
|
58
|
+
├── new [NAME] [--type <CONFIG-TYPES>] [--yes] [--interactive/--no-interactive] # Create a named typed profile without activating it.
|
|
59
|
+
├── paste [--value <VALUE>] [--stdin] [--profile <PROFILE>] [--yes] [--interactive/--no-interactive] # Paste loose env text and import recognized keys.
|
|
60
|
+
├── use [NAME] [--type <CONFIG-TYPES>] [--interactive/--no-interactive] # Activate a named profile for one config type.
|
|
61
|
+
├── list [--type <CONFIG-TYPES>] # List active default and named profiles grouped by config type.
|
|
62
|
+
├── status [--type <CONFIG-TYPES>] [--detail] # Show registered config platforms and provider ownership.
|
|
63
|
+
├── token # Manage generic runtime token profiles.
|
|
64
|
+
│ ├── status <SERVICE> [PROFILE] [--format <OUTPUT-FORMAT>] # Show safe runtime token metadata for SERVICE/PROFILE.
|
|
65
|
+
│ ├── refresh <SERVICE> [PROFILE] [--format <OUTPUT-FORMAT>] # Refresh SERVICE/PROFILE through a registered service refresh provider.
|
|
66
|
+
│ ├── import <SERVICE> [PROFILE] [--stdin] [--file <VALUE-FILE>] [--token-type <TOKEN-TYPE>] [--summary <SUMMARY>] [--expires-at <EXPIRES-AT>] [--format <OUTPUT-FORMAT>] # Explicitly import externally refreshed runtime token JSON.
|
|
67
|
+
│ ├── list [SERVICE] [--format <OUTPUT-FORMAT>] # List runtime token profiles grouped by service.
|
|
68
|
+
│ └── clear <SERVICE> [PROFILE] [--execute] [--format <OUTPUT-FORMAT>] # Clear a generic runtime token file for SERVICE/PROFILE.
|
|
69
|
+
├── cat [NAME] [--no-mask] [--type <CONFIG-TYPES>] # Print active values, or a named typed profile with -t TYPE NAME.
|
|
70
|
+
├── get [KEY] [--interactive/--no-interactive] # Get a configuration value from active typed env files.
|
|
71
|
+
├── set [KEY-VALUE] [--interactive/--no-interactive] # Set a configuration value in the matching active typed env file.
|
|
72
|
+
├── save [NAME] [--type <CONFIG-TYPES>] [--yes] [--interactive/--no-interactive] # Save current active values as a named profile.
|
|
73
|
+
├── delete [NAME] [--type <CONFIG-TYPES>] [--yes] [--interactive/--no-interactive] # Delete a named profile for one config type.
|
|
74
|
+
└── test [--target <TARGET>] [--interactive/--no-interactive] # Test a registered configuration schema.
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
## 常用命令
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
chatenv init -t example
|
|
81
|
+
chatenv status --detail
|
|
82
|
+
chatenv cat -t example
|
|
83
|
+
chatenv paste --stdin --profile work --yes
|
|
84
|
+
chatenv set EXAMPLE_API_KEY=sk-xxx
|
|
85
|
+
chatenv get EXAMPLE_API_KEY
|
|
86
|
+
chatenv token refresh PyPI RexWzh
|
|
87
|
+
chatenv token status PyPI RexWzh
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
敏感值默认 mask;`token status/list/clear` 只输出 safe metadata,不输出 raw token/cookie/CSRF values。
|
|
91
|
+
|
|
92
|
+
## Python API
|
|
93
|
+
|
|
94
|
+
```python
|
|
95
|
+
from chatenv import BaseEnvConfig, EnvField, EnvStore, get_paths
|
|
96
|
+
|
|
97
|
+
class ExampleConfig(BaseEnvConfig):
|
|
98
|
+
_title = "Example Configuration"
|
|
99
|
+
_aliases = ["example"]
|
|
100
|
+
_storage_dir = "Example"
|
|
101
|
+
|
|
102
|
+
EXAMPLE_API_KEY = EnvField("EXAMPLE_API_KEY", is_sensitive=True)
|
|
103
|
+
|
|
104
|
+
paths = get_paths()
|
|
105
|
+
store = EnvStore(paths.envs_dir)
|
|
106
|
+
store.save_active(ExampleConfig, {"EXAMPLE_API_KEY": "sk-..."})
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 文档
|
|
110
|
+
|
|
111
|
+
- https://arch.gh.wzhecnu.cn/ChatEnv/
|
|
112
|
+
- `docs/cli.md`:CLI 用法
|
|
113
|
+
- `docs/design.md`:路径、数据布局与注册策略
|
|
114
|
+
- `docs/developer-guide.md`:chatxxx 项目接入和 provider 开发指南
|
|
115
|
+
- `docs/development.md`:测试、构建与发布
|
|
116
|
+
|
|
117
|
+
## 开发
|
|
118
|
+
|
|
119
|
+
```bash
|
|
120
|
+
python -m pip install -e .[dev,docs]
|
|
121
|
+
python -m pytest -q
|
|
122
|
+
python -m mkdocs build --strict
|
|
123
|
+
python -m build
|
|
124
|
+
python -m twine check dist/*
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
## 开源协议
|
|
128
|
+
|
|
129
|
+
MIT License
|
|
@@ -22,9 +22,9 @@ classifiers = [
|
|
|
22
22
|
]
|
|
23
23
|
|
|
24
24
|
[project.urls]
|
|
25
|
-
Homepage = "https://
|
|
25
|
+
Homepage = "https://arch.gh.wzhecnu.cn/ChatEnv/"
|
|
26
|
+
Documentation = "https://arch.gh.wzhecnu.cn/ChatEnv/"
|
|
26
27
|
Repository = "https://github.com/ChatArch/ChatEnv"
|
|
27
|
-
Documentation = "https://chatarch.github.io/ChatEnv/"
|
|
28
28
|
|
|
29
29
|
[tool.setuptools.dynamic]
|
|
30
30
|
version = {attr = "chatenv.__version__"}
|
|
@@ -46,7 +46,8 @@ dev = [
|
|
|
46
46
|
]
|
|
47
47
|
docs = [
|
|
48
48
|
"mkdocs>=1.6,<2.0",
|
|
49
|
-
"mkdocs-material>=9.5,<
|
|
49
|
+
"mkdocs-material>=9.5,<10.0",
|
|
50
|
+
"mkdocs-static-i18n>=1.2,<2.0",
|
|
50
51
|
"mkdocs-minify-plugin>=0.8,<1.0",
|
|
51
52
|
"mkdocs-redirects>=1.2,<2.0",
|
|
52
53
|
"mike>=2.1,<3.0",
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
from .fields import BaseEnvConfig, EnvField
|
|
4
4
|
from .paths import ChatArchPaths, get_paths
|
|
5
5
|
from .store import EnvStore
|
|
6
|
+
from .token_refreshers import TokenRefreshResult
|
|
6
7
|
from .tokens import TokenStore
|
|
7
8
|
from .configs import FeishuConfig, OpenAIConfig
|
|
8
9
|
from .discovery import get_provider_configs, get_provider_errors, load_config_providers
|
|
@@ -14,6 +15,7 @@ __all__ = [
|
|
|
14
15
|
"EnvStore",
|
|
15
16
|
"FeishuConfig",
|
|
16
17
|
"OpenAIConfig",
|
|
18
|
+
"TokenRefreshResult",
|
|
17
19
|
"TokenStore",
|
|
18
20
|
"get_provider_configs",
|
|
19
21
|
"get_provider_errors",
|
|
@@ -22,4 +24,4 @@ __all__ = [
|
|
|
22
24
|
"__version__",
|
|
23
25
|
]
|
|
24
26
|
|
|
25
|
-
__version__ = "0.2.
|
|
27
|
+
__version__ = "0.2.8"
|
|
@@ -27,6 +27,7 @@ from .paste import iter_fields_for_values, parse_pasted_env_text
|
|
|
27
27
|
from .paths import get_paths
|
|
28
28
|
from .registry import resolve_config_types
|
|
29
29
|
from .store import EnvStore
|
|
30
|
+
from .token_refreshers import refresh_token as refresh_runtime_token
|
|
30
31
|
from .tokens import TokenStore
|
|
31
32
|
from .utils import mask_secret
|
|
32
33
|
|
|
@@ -129,7 +130,7 @@ class OrderedGroup(click.Group):
|
|
|
129
130
|
|
|
130
131
|
|
|
131
132
|
class TokenCommandGroup(OrderedGroup):
|
|
132
|
-
command_order = ["status", "refresh", "list", "clear"]
|
|
133
|
+
command_order = ["status", "refresh", "import", "list", "clear"]
|
|
133
134
|
|
|
134
135
|
|
|
135
136
|
def _format_metavar(name: str) -> str:
|
|
@@ -625,7 +626,7 @@ def _render_token_status(payload: dict[str, object]) -> None:
|
|
|
625
626
|
|
|
626
627
|
def _read_token_values(*, read_stdin: bool, value_file: Path | None) -> dict[str, object]:
|
|
627
628
|
if read_stdin == bool(value_file):
|
|
628
|
-
raise click.ClickException("token
|
|
629
|
+
raise click.ClickException("token import requires exactly one of --stdin or --file.")
|
|
629
630
|
text = sys.stdin.read() if read_stdin else value_file.read_text(encoding="utf-8") # type: ignore[union-attr]
|
|
630
631
|
try:
|
|
631
632
|
payload = json.loads(text)
|
|
@@ -639,6 +640,29 @@ def _read_token_values(*, read_stdin: bool, value_file: Path | None) -> dict[str
|
|
|
639
640
|
@token_group.command(name="refresh")
|
|
640
641
|
@click.argument("service")
|
|
641
642
|
@click.argument("profile", required=False, default="default")
|
|
643
|
+
@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text", show_default=True)
|
|
644
|
+
@click.pass_context
|
|
645
|
+
def token_refresh(ctx: click.Context, service: str, profile: str, output_format: str):
|
|
646
|
+
"""Refresh SERVICE/PROFILE through a registered service refresh provider."""
|
|
647
|
+
try:
|
|
648
|
+
payload = refresh_runtime_token(
|
|
649
|
+
service,
|
|
650
|
+
profile,
|
|
651
|
+
home=ctx.obj["paths"].home_dir,
|
|
652
|
+
env_store=_store(ctx),
|
|
653
|
+
token_store=_token_store(ctx),
|
|
654
|
+
)
|
|
655
|
+
except ValueError as exc:
|
|
656
|
+
raise click.ClickException(str(exc)) from exc
|
|
657
|
+
if output_format == "json":
|
|
658
|
+
_echo_json(payload)
|
|
659
|
+
else:
|
|
660
|
+
_render_token_status(payload)
|
|
661
|
+
|
|
662
|
+
|
|
663
|
+
@token_group.command(name="import")
|
|
664
|
+
@click.argument("service")
|
|
665
|
+
@click.argument("profile", required=False, default="default")
|
|
642
666
|
@click.option("--stdin", "read_stdin", is_flag=True, help="Read token JSON object from stdin.")
|
|
643
667
|
@click.option("--file", "value_file", type=click.Path(dir_okay=False, path_type=Path), help="Read token JSON object from a file.")
|
|
644
668
|
@click.option("--token-type", default="runtime", show_default=True, help="Caller-defined token/session type.")
|
|
@@ -646,7 +670,7 @@ def _read_token_values(*, read_stdin: bool, value_file: Path | None) -> dict[str
|
|
|
646
670
|
@click.option("--expires-at", default="", help="Optional caller-provided expiry timestamp.")
|
|
647
671
|
@click.option("--format", "output_format", type=click.Choice(["text", "json"]), default="text", show_default=True)
|
|
648
672
|
@click.pass_context
|
|
649
|
-
def
|
|
673
|
+
def token_import(
|
|
650
674
|
ctx: click.Context,
|
|
651
675
|
service: str,
|
|
652
676
|
profile: str,
|
|
@@ -657,7 +681,7 @@ def token_refresh(
|
|
|
657
681
|
expires_at: str,
|
|
658
682
|
output_format: str,
|
|
659
683
|
):
|
|
660
|
-
"""
|
|
684
|
+
"""Explicitly import externally refreshed runtime token JSON."""
|
|
661
685
|
values = _read_token_values(read_stdin=read_stdin, value_file=value_file)
|
|
662
686
|
try:
|
|
663
687
|
payload = _token_store(ctx).write(
|
|
@@ -667,7 +691,7 @@ def token_refresh(
|
|
|
667
691
|
token_type=token_type,
|
|
668
692
|
summary=_parse_summary(summary),
|
|
669
693
|
expires_at=expires_at,
|
|
670
|
-
source="
|
|
694
|
+
source="import",
|
|
671
695
|
)
|
|
672
696
|
except ValueError as exc:
|
|
673
697
|
raise click.ClickException(str(exc)) from exc
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
"""Service-owned runtime token refresh hooks.
|
|
2
|
+
|
|
3
|
+
ChatEnv owns token-store persistence and safe metadata rendering. Service
|
|
4
|
+
packages own authentication semantics. A service can register a refresh
|
|
5
|
+
provider through the ``chatenv.token_refreshers`` entry-point group; ChatEnv
|
|
6
|
+
then invokes that provider for ``chatenv token refresh SERVICE PROFILE`` and
|
|
7
|
+
writes the returned opaque runtime values into ``tokens/<Service>/<profile>.json``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from __future__ import annotations
|
|
11
|
+
|
|
12
|
+
from dataclasses import dataclass
|
|
13
|
+
from importlib.metadata import EntryPoint, entry_points
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any, Callable, Iterable, Mapping
|
|
16
|
+
|
|
17
|
+
from .paths import get_paths
|
|
18
|
+
from .store import EnvStore
|
|
19
|
+
from .tokens import DEFAULT_TOKEN_TYPE, TokenStore, normalize_service_name, normalize_token_profile
|
|
20
|
+
|
|
21
|
+
ENTRY_POINT_GROUP = "chatenv.token_refreshers"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class TokenRefreshResult:
|
|
26
|
+
"""Opaque values plus safe metadata returned by a service refresh hook."""
|
|
27
|
+
|
|
28
|
+
values: Mapping[str, Any]
|
|
29
|
+
token_type: str = DEFAULT_TOKEN_TYPE
|
|
30
|
+
summary: Mapping[str, Any] | None = None
|
|
31
|
+
expires_at: str | None = None
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
_token_refreshers: dict[str, Callable[..., Any]] = {}
|
|
35
|
+
_loaded = False
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _iter_refresh_entry_points() -> Iterable[EntryPoint]:
|
|
39
|
+
eps = entry_points()
|
|
40
|
+
if hasattr(eps, "select"):
|
|
41
|
+
return eps.select(group=ENTRY_POINT_GROUP)
|
|
42
|
+
return eps.get(ENTRY_POINT_GROUP, [])
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def clear_token_refreshers() -> None:
|
|
46
|
+
"""Clear the refresh-provider cache; intended for tests and reloads."""
|
|
47
|
+
|
|
48
|
+
global _loaded
|
|
49
|
+
_loaded = False
|
|
50
|
+
_token_refreshers.clear()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _provider_callable(obj: Any) -> Callable[..., Any]:
|
|
54
|
+
if callable(obj):
|
|
55
|
+
return obj
|
|
56
|
+
for attr in ("refresh_token", "refresh"):
|
|
57
|
+
candidate = getattr(obj, attr, None)
|
|
58
|
+
if callable(candidate):
|
|
59
|
+
return candidate
|
|
60
|
+
raise TypeError("token refresh provider must be callable or expose refresh_token()/refresh()")
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def load_token_refreshers(*, force: bool = False) -> dict[str, Callable[..., Any]]:
|
|
64
|
+
"""Load service refresh hooks registered through entry points."""
|
|
65
|
+
|
|
66
|
+
global _loaded
|
|
67
|
+
if _loaded and not force:
|
|
68
|
+
return dict(_token_refreshers)
|
|
69
|
+
if force:
|
|
70
|
+
_token_refreshers.clear()
|
|
71
|
+
_loaded = True
|
|
72
|
+
for ep in _iter_refresh_entry_points():
|
|
73
|
+
service_name = normalize_service_name(ep.name)
|
|
74
|
+
provider = _provider_callable(ep.load())
|
|
75
|
+
_token_refreshers[service_name.lower()] = provider
|
|
76
|
+
return dict(_token_refreshers)
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def get_token_refresher(service: str, *, force: bool = False) -> Callable[..., Any] | None:
|
|
80
|
+
service_name = normalize_service_name(service)
|
|
81
|
+
return load_token_refreshers(force=force).get(service_name.lower())
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def _coerce_refresh_result(result: Any) -> TokenRefreshResult:
|
|
85
|
+
if isinstance(result, TokenRefreshResult):
|
|
86
|
+
return result
|
|
87
|
+
if isinstance(result, Mapping):
|
|
88
|
+
values = result.get("values")
|
|
89
|
+
if not isinstance(values, Mapping):
|
|
90
|
+
raise ValueError("token refresh provider result must include a non-empty values object")
|
|
91
|
+
return TokenRefreshResult(
|
|
92
|
+
values=values,
|
|
93
|
+
token_type=str(result.get("token_type") or DEFAULT_TOKEN_TYPE),
|
|
94
|
+
summary=result.get("summary") if isinstance(result.get("summary"), Mapping) else None,
|
|
95
|
+
expires_at=str(result.get("expires_at") or ""),
|
|
96
|
+
)
|
|
97
|
+
raise ValueError("token refresh provider must return TokenRefreshResult or a mapping")
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def refresh_token(
|
|
101
|
+
service: str,
|
|
102
|
+
profile: str | None = None,
|
|
103
|
+
*,
|
|
104
|
+
home: str | Path | None = None,
|
|
105
|
+
env_store: EnvStore | None = None,
|
|
106
|
+
token_store: TokenStore | None = None,
|
|
107
|
+
) -> dict[str, Any]:
|
|
108
|
+
"""Invoke the registered service refresh hook and persist its token values."""
|
|
109
|
+
|
|
110
|
+
service_name = normalize_service_name(service)
|
|
111
|
+
profile_name = normalize_token_profile(profile)
|
|
112
|
+
provider = get_token_refresher(service_name)
|
|
113
|
+
if provider is None:
|
|
114
|
+
raise ValueError(f"No token refresh provider registered for {service_name}")
|
|
115
|
+
|
|
116
|
+
paths = get_paths(home)
|
|
117
|
+
env_store = env_store or EnvStore(paths.envs_dir)
|
|
118
|
+
token_store = token_store or TokenStore(tokens_dir=paths.tokens_dir)
|
|
119
|
+
result = _coerce_refresh_result(
|
|
120
|
+
provider(
|
|
121
|
+
service=service_name,
|
|
122
|
+
profile=profile_name,
|
|
123
|
+
home=paths.home_dir,
|
|
124
|
+
env_store=env_store,
|
|
125
|
+
token_store=token_store,
|
|
126
|
+
)
|
|
127
|
+
)
|
|
128
|
+
return token_store.write(
|
|
129
|
+
service_name,
|
|
130
|
+
profile_name,
|
|
131
|
+
values=dict(result.values),
|
|
132
|
+
token_type=result.token_type or DEFAULT_TOKEN_TYPE,
|
|
133
|
+
summary=dict(result.summary or {}),
|
|
134
|
+
expires_at=result.expires_at or "",
|
|
135
|
+
source="refresh",
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
__all__ = [
|
|
140
|
+
"ENTRY_POINT_GROUP",
|
|
141
|
+
"TokenRefreshResult",
|
|
142
|
+
"clear_token_refreshers",
|
|
143
|
+
"get_token_refresher",
|
|
144
|
+
"load_token_refreshers",
|
|
145
|
+
"refresh_token",
|
|
146
|
+
]
|