├── .gitignore ├── .python-version ├── LICENSE ├── Makefile ├── README.md ├── README.zh.md ├── demo ├── base │ ├── settings.yml │ └── usr │ │ ├── config.yml │ │ └── my_namespace │ │ └── hello_world │ │ ├── README.md │ │ ├── README.zh.md │ │ ├── hello_world.py │ │ └── interface.yml └── caller.py ├── devstream ├── __init__.py ├── env_manager.py ├── env_vars.py ├── main.py ├── namespace.py ├── path.py ├── schema.py ├── step.py ├── user_setting.py ├── utils.py └── workflow.py ├── pyproject.toml ├── specs ├── interface-v1.0.0.md └── runtime-v1.0.0.md └── uv.lock /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | share/python-wheels/ 24 | *.egg-info/ 25 | .installed.cfg 26 | *.egg 27 | MANIFEST 28 | 29 | # PyInstaller 30 | # Usually these files are written by a python script from a template 31 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 32 | *.manifest 33 | *.spec 34 | 35 | # Installer logs 36 | pip-log.txt 37 | pip-delete-this-directory.txt 38 | 39 | # Unit test / coverage reports 40 | htmlcov/ 41 | .tox/ 42 | .nox/ 43 | .coverage 44 | .coverage.* 45 | .cache 46 | nosetests.xml 47 | coverage.xml 48 | *.cover 49 | *.py,cover 50 | .hypothesis/ 51 | .pytest_cache/ 52 | cover/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | .pybuilder/ 76 | target/ 77 | 78 | # Jupyter Notebook 79 | .ipynb_checkpoints 80 | 81 | # IPython 82 | profile_default/ 83 | ipython_config.py 84 | 85 | # pyenv 86 | # For a library or package, you might want to ignore these files since the code is 87 | # intended to run in multiple environments; otherwise, check them in: 88 | # .python-version 89 | 90 | # pipenv 91 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 92 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 93 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 94 | # install all needed dependencies. 95 | #Pipfile.lock 96 | 97 | # UV 98 | # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | #uv.lock 102 | 103 | # poetry 104 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 105 | # This is especially recommended for binary packages to ensure reproducibility, and is more 106 | # commonly ignored for libraries. 107 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 108 | #poetry.lock 109 | 110 | # pdm 111 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 112 | #pdm.lock 113 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 114 | # in version control. 115 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 116 | .pdm.toml 117 | .pdm-python 118 | .pdm-build/ 119 | 120 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 121 | __pypackages__/ 122 | 123 | # Celery stuff 124 | celerybeat-schedule 125 | celerybeat.pid 126 | 127 | # SageMath parsed files 128 | *.sage.py 129 | 130 | # Environments 131 | .env 132 | .venv 133 | env/ 134 | venv/ 135 | ENV/ 136 | env.bak/ 137 | venv.bak/ 138 | 139 | # Spyder project settings 140 | .spyderproject 141 | .spyproject 142 | 143 | # Rope project settings 144 | .ropeproject 145 | 146 | # mkdocs documentation 147 | /site 148 | 149 | # mypy 150 | .mypy_cache/ 151 | .dmypy.json 152 | dmypy.json 153 | 154 | # Pyre type checker 155 | .pyre/ 156 | 157 | # pytype static type analyzer 158 | .pytype/ 159 | 160 | # Cython debug symbols 161 | cython_debug/ 162 | 163 | # PyCharm 164 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 165 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 166 | # and can be added to the global gitignore or merged into this file. For a more nuclear 167 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 168 | #.idea/ 169 | 170 | # PyPI configuration file 171 | .pypirc 172 | 173 | .DS_Store 174 | tmp/ 175 | data/ -------------------------------------------------------------------------------- /.python-version: -------------------------------------------------------------------------------- 1 | 3.10 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: check fix 2 | 3 | div = $(shell printf '=%.0s' {1..120}) 4 | 5 | DIR="." 6 | check: 7 | @echo ${div} 8 | uv run --active ruff check $(DIR) 9 | uv run --active ruff format $(DIR) --check 10 | @echo "Done!" 11 | 12 | fix: 13 | @echo ${div} 14 | uv run --active ruff format $(DIR) 15 | @echo ${div} 16 | uv run --active ruff check $(DIR) --fix 17 | @echo "Done!" -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # devstream 2 | Intelligent Workflow Engine Driven by Natural Language. 3 | -------------------------------------------------------------------------------- /README.zh.md: -------------------------------------------------------------------------------- 1 | # Devstream 智能工作流引擎 2 | 3 | > 注意:DevChat IDE 插件尚未迁移至本实现,在当前插件中创建智能工作流请参见[文档说明](https://docs.devchat.ai/zh/quick-start/create_workflow)。 4 | 5 | 本实现遵循[Devstream 接口规范》版本v1.0.0](specs/interface-v1.0.0.zh.md)。未实现 [`shell-python` 运行时系统](specs/runtime-v0.1.0.zh.md)的 Python 环境和包自动管理能力。 6 | 7 | 本工作流引擎采用 Python 编程语言实现,可作为 Python 包被用户脚本或其他工作流引入和使用。引擎默认使用调用时所处 Python 环境的解释器。 8 | 9 | ## 示例 10 | 11 | 本项目 `/demo` 目录是一个包含了工作流 `/hello_world` 实现的示例,下面操作步骤可实现对该工作流的调用。命令行以 Linux 或 macOS 操作系统终端为例。 12 | 13 | 1. 设置运行时依赖的环境变量,在终端执行如下命令: 14 | - `export DEVSTREAM_BASE=/path/to/demo/`,其中 `/path/to/demo/` 替换为本地 `demo` 目录的完整路径。 15 | 2. 本地应安装好 Python 环境,并在本地 `demo` 目录下的 `settings.yml` 文件中注册,该文件形如: 16 | 17 | ```yaml 18 | environments: 19 | myenv: "/path/to/python/bin" 20 | ``` 21 | 22 | 其中 `/path/to/python/bin` 替换为本地 Python 环境解释器的完整路径。 23 | 3. 我们假设一个用户脚本或另外的工作流希望调用 `/hello_world`,那么 [`/demo/caller.py`](demo/caller.py) 是一个实现这种调用的实例: 24 | ```python 25 | import devstream 26 | 27 | devstream.call("/hello_world", "This is some user input.") 28 | ``` 29 | 4. 在终端中执行如下命令行,可实测调用 `/hello_world` 的结果: 30 | - `python /path/to/caller.py`,其中 `python` 指向终端当前所处的 Python 环境,也是运行 Devstream 引擎的环境;`/path/to/caller.py` 替换为脚本实际所处的路径。 31 | -------------------------------------------------------------------------------- /demo/base/settings.yml: -------------------------------------------------------------------------------- 1 | environments: 2 | myenv: "/path/to/python/bin" -------------------------------------------------------------------------------- /demo/base/usr/config.yml: -------------------------------------------------------------------------------- 1 | namespaces: 2 | - my_namespace -------------------------------------------------------------------------------- /demo/base/usr/my_namespace/hello_world/README.md: -------------------------------------------------------------------------------- 1 | # Hello World! -------------------------------------------------------------------------------- /demo/base/usr/my_namespace/hello_world/README.zh.md: -------------------------------------------------------------------------------- 1 | # 你好,世界! -------------------------------------------------------------------------------- /demo/base/usr/my_namespace/hello_world/hello_world.py: -------------------------------------------------------------------------------- 1 | import sys 2 | 3 | 4 | def main(): 5 | print("Hello world from devstream demo.") 6 | if len(sys.argv) > 1: 7 | print(f"User input: {sys.argv[1]}") 8 | else: 9 | print("No user input provided.") 10 | 11 | 12 | if __name__ == "__main__": 13 | main() 14 | -------------------------------------------------------------------------------- /demo/base/usr/my_namespace/hello_world/interface.yml: -------------------------------------------------------------------------------- 1 | description: a hello-world demo 2 | 3 | help: 4 | - en: README.md 5 | - zh: README.zh.md 6 | 7 | parameters: 8 | __input__: required 9 | 10 | runtime: 11 | id: shell-python 12 | venv: myenv 13 | run: 14 | - $__python__ $__idir__/hello_world.py $__input__ 15 | -------------------------------------------------------------------------------- /demo/caller.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | # Set DEVSTREAM_BASE to /demo/base 4 | demo_base = os.path.join(os.path.dirname(__file__), "base") 5 | os.environ["DEVSTREAM_BASE"] = demo_base 6 | 7 | import devstream # noqa: E402 8 | 9 | 10 | def main(): 11 | # res = devstream.call("/hello_world", "--help") 12 | # print(res) 13 | _ = devstream.call("/hello_world", "This is some user input.") 14 | 15 | 16 | if __name__ == "__main__": 17 | main() 18 | -------------------------------------------------------------------------------- /devstream/__init__.py: -------------------------------------------------------------------------------- 1 | from .main import call as call 2 | -------------------------------------------------------------------------------- /devstream/env_manager.py: -------------------------------------------------------------------------------- 1 | import hashlib 2 | import logging 3 | import os 4 | import shutil 5 | import subprocess 6 | import sys 7 | from typing import Dict, Optional, Tuple 8 | 9 | import virtualenv 10 | 11 | from .env_vars import MAMBA_BIN_PATH 12 | from .path import CHAT_CONFIG_FILENAME, CHAT_DIR, ENV_CACHE_DIR, MAMBA_PY_ENVS, MAMBA_ROOT 13 | from .schema import ExternalPyConf 14 | from .user_setting import USER_SETTINGS 15 | 16 | logger = logging.getLogger(__name__) 17 | 18 | PYPI_TUNA = "https://pypi.tuna.tsinghua.edu.cn/simple" 19 | DEFAULT_CONDA_FORGE_URL = "https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud/conda-forge/" 20 | 21 | 22 | def _get_external_envs() -> Dict[str, ExternalPyConf]: 23 | """ 24 | Get the external python environments info from the user settings. 25 | """ 26 | external_pythons: Dict[str, ExternalPyConf] = {} 27 | for env_name, python_bin in USER_SETTINGS.environments.items(): 28 | external_pythons[env_name] = ExternalPyConf(env_name=env_name, py_bin=python_bin) 29 | 30 | return external_pythons 31 | 32 | 33 | EXTERNAL_ENVS = _get_external_envs() 34 | 35 | 36 | class PyEnvManager: 37 | mamba_bin = MAMBA_BIN_PATH 38 | mamba_root = MAMBA_ROOT 39 | 40 | def __init__(self): 41 | pass 42 | 43 | @staticmethod 44 | def get_py_version(py: str) -> Optional[str]: 45 | """ 46 | Get the version of the python executable. 47 | """ 48 | py_version_cmd = [py, "--version"] 49 | with subprocess.Popen(py_version_cmd, stdout=subprocess.PIPE, stderr=None) as proc: 50 | proc.wait() 51 | 52 | if proc.returncode != 0: 53 | return None 54 | 55 | out = proc.stdout.read().decode("utf-8") 56 | return out.split()[1] 57 | 58 | @staticmethod 59 | def get_dep_hash(reqirements_file: str) -> str: 60 | """ 61 | Get the hash of the requirements file content. 62 | 63 | Used to check if the requirements file has been changed. 64 | """ 65 | with open(reqirements_file, "r", encoding="utf-8") as f: 66 | content = f.read() 67 | return hashlib.md5(content.encode("utf-8")).hexdigest() 68 | 69 | def ensure( 70 | self, 71 | env_name: str, 72 | py_version: Optional[str] = None, 73 | reqirements_file: Optional[str] = None, 74 | ) -> Optional[str]: 75 | """ 76 | Ensure the python environment exists with the given name and version. 77 | And install the requirements if provided. 78 | 79 | return the python executable path. 80 | """ 81 | py = self.get_py(env_name) 82 | 83 | should_remove_old = False 84 | should_install_deps = False 85 | 86 | if py: 87 | # check the version of the python executable 88 | current_version = self.get_py_version(py) 89 | 90 | if py_version and current_version != py_version: 91 | should_remove_old = True 92 | 93 | if reqirements_file and self.should_reinstall(env_name, reqirements_file): 94 | should_install_deps = True 95 | 96 | if not should_remove_old and not should_install_deps: 97 | return py 98 | 99 | # log_file = get_logging_file() 100 | 101 | if should_remove_old: 102 | self.remove(env_name, py_version) 103 | 104 | # create the environment if it doesn't exist or needs to be recreated 105 | if should_remove_old or not py: 106 | if py_version: 107 | create_ok, msg = self.create(env_name, py_version) 108 | else: 109 | create_ok, msg = self.create_with_virtualenv(env_name) 110 | 111 | if not create_ok: 112 | logger.error(f"Failed to create {env_name}: {msg}") 113 | sys.exit(0) 114 | 115 | # install or update the requirements 116 | if reqirements_file: 117 | filename = os.path.basename(reqirements_file) 118 | action = "Updating" if should_install_deps else "Installing" 119 | logger.debug(f"- {action} dependencies from {filename}...") 120 | install_ok, msg = self.install(env_name, reqirements_file) 121 | if not install_ok: 122 | logger.error(f"Failed to {action.lower()} dependencies: {msg}") 123 | sys.exit(0) 124 | else: 125 | # save the hash of the requirements file content 126 | dep_hash = self.get_dep_hash(reqirements_file) 127 | cache_file = os.path.join(ENV_CACHE_DIR, f"{env_name}") 128 | with open(cache_file, "w", encoding="utf-8") as f: 129 | f.write(dep_hash) 130 | 131 | return self.get_py(env_name) 132 | 133 | def create_with_virtualenv(self, env_name: str) -> Tuple[bool, str]: 134 | """ 135 | Create a new python environment using virtualenv with the current Python interpreter. 136 | """ 137 | env_path = os.path.join(MAMBA_PY_ENVS, env_name) 138 | if os.path.exists(env_path): 139 | return True, "" 140 | 141 | try: 142 | # Use virtualenv.cli_run to create a virtual environment 143 | virtualenv.cli_run([env_path, "--python", sys.executable]) 144 | 145 | # Create sitecustomize.py in the lib/site-packages directory 146 | site_packages_dir = os.path.join(env_path, "Lib", "site-packages") 147 | if os.path.exists(site_packages_dir): 148 | sitecustomize_path = os.path.join(site_packages_dir, "sitecustomize.py") 149 | with open(sitecustomize_path, "w") as f: 150 | f.write("import sys\n") 151 | f.write('sys.path = [path for path in sys.path if path.find("conda") == -1]') 152 | 153 | return True, "" 154 | except Exception as e: 155 | return False, str(e) 156 | 157 | def install(self, env_name: str, requirements_file: str) -> Tuple[bool, str]: 158 | """ 159 | Install or update requirements in the python environment. 160 | 161 | Args: 162 | env_name: the name of the python environment 163 | requirements_file: the absolute path to the requirements file. 164 | 165 | Returns: 166 | A tuple (success, message), where success is a boolean indicating 167 | whether the installation was successful, and message is a string 168 | containing output or error information. 169 | """ 170 | py = self.get_py(env_name) 171 | if not py: 172 | return False, "Python executable not found." 173 | 174 | if not os.path.exists(requirements_file): 175 | return False, "Dependencies file not found." 176 | 177 | # Base command 178 | cmd = [ 179 | py, 180 | "-m", 181 | "pip", 182 | "install", 183 | "-r", 184 | requirements_file, 185 | "-i", 186 | PYPI_TUNA, 187 | "--no-warn-script-location", 188 | ] 189 | 190 | # Check if this is an update or a fresh install 191 | cache_file = os.path.join(ENV_CACHE_DIR, f"{env_name}") 192 | if os.path.exists(cache_file): 193 | # This is an update, add --upgrade flag 194 | cmd.append("--upgrade") 195 | 196 | env = os.environ.copy() 197 | env.pop("PYTHONPATH", None) 198 | with subprocess.Popen( 199 | cmd, stdout=subprocess.DEVNULL, stderr=subprocess.PIPE, env=env 200 | ) as proc: 201 | _, err = proc.communicate() 202 | 203 | if proc.returncode != 0: 204 | return False, f"Installation failed: {err.decode('utf-8')}" 205 | 206 | return True, "Installation successful" 207 | 208 | def should_reinstall(self, env_name: str, requirements_file: str) -> bool: 209 | """ 210 | Check if the requirements file has been changed. 211 | """ 212 | cache_file = os.path.join(ENV_CACHE_DIR, f"{env_name}") 213 | if not os.path.exists(cache_file): 214 | return True 215 | 216 | dep_hash = self.get_dep_hash(requirements_file) 217 | with open(cache_file, "r", encoding="utf-8") as f: 218 | cache_hash = f.read() 219 | 220 | return dep_hash != cache_hash 221 | 222 | def create(self, env_name: str, py_version: str) -> Tuple[bool, str]: 223 | """ 224 | Create a new python environment using mamba. 225 | """ 226 | is_exist = os.path.exists(os.path.join(MAMBA_PY_ENVS, env_name)) 227 | if is_exist: 228 | return True, "" 229 | 230 | # Get conda-forge URL from config file 231 | conda_forge_url = self._get_conda_forge_url() 232 | 233 | # create the environment 234 | cmd = [ 235 | self.mamba_bin, 236 | "create", 237 | "-n", 238 | env_name, 239 | "-c", 240 | conda_forge_url, 241 | "-r", 242 | self.mamba_root, 243 | f"python={py_version}", 244 | "-y", 245 | ] 246 | with subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) as proc: 247 | out, err = proc.communicate() 248 | msg = f"err: {err.decode()}\n-----\nout: {out.decode()}" 249 | 250 | if proc.returncode != 0: 251 | return False, msg 252 | return True, "" 253 | 254 | def remove(self, env_name: str, py_version: Optional[str] = None) -> bool: 255 | if py_version: 256 | return self.remove_by_mamba(env_name) 257 | return self.remove_by_del(env_name) 258 | 259 | def remove_by_del(self, env_name: str) -> bool: 260 | """ 261 | Remove the python environment. 262 | """ 263 | env_path = os.path.join(MAMBA_PY_ENVS, env_name) 264 | try: 265 | # Remove the environment directory 266 | if os.path.exists(env_path): 267 | shutil.rmtree(env_path) 268 | return True 269 | except Exception as e: 270 | logger.warning(f"Failed to remove environment {env_name}: {e}") 271 | return False 272 | 273 | def remove_by_mamba(self, env_name: str) -> bool: 274 | """ 275 | Remove the python environment. 276 | """ 277 | is_exist = os.path.exists(os.path.join(MAMBA_PY_ENVS, env_name)) 278 | if not is_exist: 279 | return True 280 | 281 | # remove the environment 282 | cmd = [ 283 | self.mamba_bin, 284 | "env", 285 | "remove", 286 | "-n", 287 | env_name, 288 | "-r", 289 | self.mamba_root, 290 | "-y", 291 | ] 292 | with subprocess.Popen(cmd, stdout=subprocess.DEVNULL, stderr=None) as proc: 293 | proc.wait() 294 | 295 | if proc.returncode != 0: 296 | return False 297 | 298 | return True 299 | 300 | def get_py(self, env_name: str) -> Optional[str]: 301 | """ 302 | Get the python executable path of the given environment. 303 | """ 304 | env_path = None 305 | if sys.platform == "win32": 306 | env_path = os.path.join(MAMBA_PY_ENVS, env_name, "python.exe") 307 | if not os.path.exists(env_path): 308 | env_path = os.path.join(MAMBA_PY_ENVS, env_name, "Scripts", "python.exe") 309 | else: 310 | env_path = os.path.join(MAMBA_PY_ENVS, env_name, "bin", "python") 311 | 312 | if env_path and os.path.exists(env_path): 313 | return env_path 314 | 315 | return None 316 | 317 | def _get_conda_forge_url(self) -> str: 318 | """ 319 | Read the conda-forge URL from the config file. 320 | If the config file does not exist or does not contain the conda-forge URL, 321 | use the default value. 322 | """ 323 | config_file = os.path.join(CHAT_DIR, CHAT_CONFIG_FILENAME) 324 | 325 | try: 326 | if not os.path.exists(config_file): 327 | return DEFAULT_CONDA_FORGE_URL 328 | 329 | import yaml 330 | 331 | with open(config_file, "r", encoding="utf-8") as f: 332 | config = yaml.safe_load(f) 333 | 334 | return config.get("conda-forge-url", DEFAULT_CONDA_FORGE_URL) 335 | except Exception as e: 336 | logger.error(f"An error occurred when loading conda-forge-url from config file: {e}") 337 | return DEFAULT_CONDA_FORGE_URL 338 | -------------------------------------------------------------------------------- /devstream/env_vars.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | DEVSTREAM_BASE = os.environ.get("DEVSTREAM_BASE", None) 4 | MAMBA_BIN_PATH = os.environ.get("MAMBA_BIN_PATH", "") 5 | -------------------------------------------------------------------------------- /devstream/main.py: -------------------------------------------------------------------------------- 1 | from typing import Optional 2 | 3 | from .workflow import Workflow 4 | 5 | 6 | def call(workflow_name: str, user_input: Optional[str] = None): 7 | user_input = user_input or "" 8 | 9 | wf_name, _ = Workflow.parse_trigger(workflow_name) 10 | workflow = Workflow.load(wf_name) if wf_name else None 11 | 12 | if not workflow: 13 | raise ValueError(f"Workflow <{workflow_name}> not found.") 14 | 15 | if workflow.should_show_help(user_input): 16 | doc = workflow.get_help_doc(user_input) 17 | return doc 18 | 19 | workflow.setup(user_input, None, None, None) 20 | 21 | return workflow.run() 22 | -------------------------------------------------------------------------------- /devstream/namespace.py: -------------------------------------------------------------------------------- 1 | """ 2 | Namespace management for workflows 3 | """ 4 | 5 | import logging 6 | import os 7 | from pathlib import Path 8 | from typing import Dict, List, Set, Tuple 9 | 10 | import oyaml as yaml 11 | import yaml as pyyaml 12 | from pydantic import BaseModel, Field, ValidationError 13 | 14 | from .path import ( 15 | COMMUNITY_WORKFLOWS, 16 | INTERFACE_FILENAMES, 17 | SYS_WORKFLOWS, 18 | USER_BASE, 19 | USER_CONFIG_FILE, 20 | ) 21 | 22 | logger = logging.getLogger(__name__) 23 | 24 | 25 | class CustomConfig(BaseModel): 26 | namespaces: List[str] = [] # active namespaces ordered by priority 27 | 28 | 29 | class WorkflowMeta(BaseModel): 30 | name: str = Field(..., description="workflow name") 31 | namespace: str = Field(..., description="workflow namespace") 32 | active: bool = Field(..., description="active flag") 33 | command_conf: Dict = Field(description="command configuration", default_factory=dict) 34 | 35 | def __str__(self): 36 | return f"{'*' if self.active else ' '} {self.name} ({self.namespace})" 37 | 38 | 39 | def _load_custom_config() -> CustomConfig: 40 | """ 41 | Load the custom config file. 42 | """ 43 | config = CustomConfig() 44 | 45 | if not os.path.exists(USER_CONFIG_FILE): 46 | return config 47 | 48 | with open(USER_CONFIG_FILE, "r", encoding="utf-8") as file: 49 | content = file.read() 50 | yaml_content = yaml.safe_load(content) 51 | try: 52 | if yaml_content: 53 | config = CustomConfig.model_validate(yaml_content) 54 | except ValidationError as err: 55 | logger.warning("Invalid custom config file: %s", err) 56 | 57 | return config 58 | 59 | 60 | def get_prioritized_namespace_path() -> List[str]: 61 | """ 62 | Get the prioritized namespaces. 63 | 64 | priority: usr > sys > community 65 | """ 66 | config = _load_custom_config() 67 | 68 | namespaces = config.namespaces 69 | 70 | namespace_paths = [os.path.join(USER_BASE, ns) for ns in namespaces] 71 | 72 | namespace_paths.append(SYS_WORKFLOWS) 73 | namespace_paths.append(COMMUNITY_WORKFLOWS) 74 | 75 | return namespace_paths 76 | 77 | 78 | def iter_namespace(ns_path: str, existing_names: Set[str]) -> Tuple[List[WorkflowMeta], Set[str]]: 79 | """ 80 | Get all workflows under the namespace path. 81 | 82 | Args: 83 | ns_path: the namespace path 84 | existing_names: the existing workflow names to check if the workflow is the first priority 85 | 86 | Returns: 87 | List[WorkflowMeta]: the workflows 88 | Set[str]: the updated existing workflow names 89 | """ 90 | root = Path(ns_path) 91 | interest_files = set(INTERFACE_FILENAMES) 92 | result = [] 93 | unique_names = set(existing_names) 94 | for file in root.rglob("*"): 95 | try: 96 | if file.is_file() and file.name in interest_files: 97 | rel_path = file.relative_to(root) 98 | parts = rel_path.parts 99 | workflow_name = ".".join(parts[:-1]) 100 | is_first = workflow_name not in unique_names 101 | 102 | # load the config content from file 103 | with open(file, "r", encoding="utf-8") as file_handle: 104 | yaml_content = file_handle.read() 105 | command_conf = yaml.safe_load(yaml_content) 106 | # pop the "steps" field 107 | command_conf.pop("steps", None) 108 | 109 | workflow = WorkflowMeta( 110 | name=workflow_name, 111 | namespace=root.name, 112 | active=is_first, 113 | command_conf=command_conf, 114 | ) 115 | unique_names.add(workflow_name) 116 | result.append(workflow) 117 | except pyyaml.scanner.ScannerError as err: 118 | logger.error(f"Failed to load {rel_path}: {err}") 119 | except Exception as err: 120 | logger.error(f"Unknown error when loading {rel_path}: {err}") 121 | 122 | return result, unique_names 123 | -------------------------------------------------------------------------------- /devstream/path.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from .env_vars import DEVSTREAM_BASE 4 | 5 | # ------------------------------- 6 | # devchat basic paths 7 | # ------------------------------- 8 | USE_DIR = os.path.expanduser("~") 9 | CHAT_DIR = os.path.join(USE_DIR, ".chat") 10 | CHAT_CONFIG_FILENAME = "config.yml" 11 | DEVCHAT_WORKFLOWS_BASE_NAME = "scripts" 12 | 13 | 14 | # ------------------------------- 15 | # workflow paths 16 | # ------------------------------- 17 | WORKFLOWS_BASE = DEVSTREAM_BASE or os.path.join(CHAT_DIR, DEVCHAT_WORKFLOWS_BASE_NAME) 18 | 19 | SYS_WORKFLOWS = os.path.join(WORKFLOWS_BASE, "sys") 20 | COMMUNITY_WORKFLOWS = os.path.join(WORKFLOWS_BASE, "comm") 21 | 22 | INTERFACE_FILENAMES = [ 23 | # the order matters 24 | "interface.yml", 25 | "interface.yaml", 26 | "command.yml", 27 | "command.yaml", 28 | ] 29 | 30 | 31 | # ------------------------------- 32 | # workflow related cache data 33 | # ------------------------------- 34 | CACHE_DIR = os.path.join(WORKFLOWS_BASE, "cache") 35 | ENV_CACHE_DIR = os.path.join(CACHE_DIR, "env_cache") 36 | os.makedirs(ENV_CACHE_DIR, exist_ok=True) 37 | 38 | 39 | # ------------------------------- 40 | # custom/usr paths 41 | # ------------------------------- 42 | USER_BASE = os.path.join(WORKFLOWS_BASE, "usr") 43 | USER_CONFIG_FILE = os.path.join(USER_BASE, "config.yml") 44 | USER_SETTINGS_FILE = os.path.join(WORKFLOWS_BASE, "settings.yml") 45 | 46 | 47 | # ---- --------------------------- 48 | # Python environments paths 49 | # ------------------------------- 50 | MAMBA_ROOT = os.path.join(CHAT_DIR, "mamba") 51 | MAMBA_PY_ENVS = os.path.join(MAMBA_ROOT, "envs") 52 | -------------------------------------------------------------------------------- /devstream/schema.py: -------------------------------------------------------------------------------- 1 | import re 2 | from typing import Dict, List, Optional, Union 3 | 4 | from pydantic import BaseModel, ConfigDict, Field, computed_field, field_validator 5 | 6 | 7 | class Parameters(BaseModel): 8 | input_mode: str = Field(default="optional", alias="__input__") # "required" | "optional" 9 | 10 | 11 | class RuntimeConf(BaseModel): 12 | id: str = "shell-python" 13 | run: List[str] = Field(default_factory=list) 14 | venv: Optional[str] = None 15 | # python: Optional[str] = None 16 | python_version: Optional[str] = Field( 17 | default=None, alias="python" 18 | ) # the specific python version. 19 | requirements: Optional[str] = None # absolute path to the requirements file 20 | 21 | @field_validator("python_version") 22 | @classmethod 23 | def validate_python_version(cls, value: Optional[str]) -> Optional[str]: 24 | if value is None: 25 | return value 26 | 27 | pattern = r"^\d+\.\d+(\.\d+)?$" 28 | if not re.match(pattern, value): 29 | raise ValueError(f"Invalid version format: {value}. Should use the specific version.") 30 | return value 31 | 32 | 33 | class WorkflowConfig(BaseModel): 34 | description: str 35 | runtime: RuntimeConf 36 | dirpath: str # the path of the workflow dir 37 | parameters: Parameters = Field(default_factory=Parameters) 38 | help: Optional[Union[str, List[Dict[str, str]]]] = None 39 | 40 | @computed_field 41 | @property 42 | def input_required(self) -> bool: 43 | return self.parameters.input_mode.lower() == "required" 44 | 45 | model_config = ConfigDict(extra="ignore") 46 | 47 | 48 | class UserSettings(BaseModel): 49 | environments: Dict[str, str] = Field(default_factory=dict) 50 | 51 | model_config = ConfigDict(extra="ignore") 52 | 53 | 54 | class RuntimeParameter(BaseModel): 55 | workflow_python: str 56 | user_input: Optional[str] = None 57 | model_name: Optional[str] = None 58 | history_messages: Optional[Dict] = None 59 | parent_hash: Optional[str] = None 60 | 61 | model_config = ConfigDict(extra="ignore") 62 | 63 | 64 | class ExternalPyConf(BaseModel): 65 | env_name: str # the env_name of workflow python to act as 66 | py_bin: str # the python executable path 67 | 68 | model_config = ConfigDict(extra="ignore") 69 | -------------------------------------------------------------------------------- /devstream/step.py: -------------------------------------------------------------------------------- 1 | import json 2 | import os 3 | import shlex 4 | import subprocess 5 | from enum import Enum 6 | from typing import Dict, List, Tuple 7 | 8 | from .path import WORKFLOWS_BASE 9 | from .schema import RuntimeParameter, WorkflowConfig 10 | 11 | 12 | class BuiltInVars(str, Enum): 13 | """ 14 | Built-in variables within the workflow step command. 15 | """ 16 | 17 | wf_python = "$__python__" 18 | wf_dirpath = "$__idir__" 19 | user_input = "$__input__" 20 | 21 | 22 | class BuiltInEnvs(str, Enum): 23 | """ 24 | Built-in environment variables for the step subprocess. 25 | """ 26 | 27 | llm_model = "LLM_MODEL" 28 | parent_hash = "PARENT_HASH" 29 | context_contents = "CONTEXT_CONTENTS" 30 | 31 | 32 | class WorkflowStep: 33 | def __init__(self, cmd: str): 34 | """ 35 | Initialize a workflow step with the given configuration. 36 | """ 37 | self._kwargs = {"run": cmd} 38 | 39 | @property 40 | def command_raw(self) -> str: 41 | """ 42 | The raw command string from the config. 43 | """ 44 | return self._kwargs.get("run", "") 45 | 46 | def _setup_env(self, wf_config: WorkflowConfig, rt_param: RuntimeParameter) -> Dict[str, str]: 47 | """ 48 | Setup the environment variables for the subprocess. 49 | """ 50 | env = os.environ.copy() 51 | 52 | # set PYTHONPATH for the subprocess 53 | new_paths = [WORKFLOWS_BASE] 54 | 55 | paths = [os.path.normpath(p) for p in new_paths] 56 | paths = [p.replace("\\", "\\\\") for p in paths] 57 | joined = os.pathsep.join(paths) 58 | 59 | env["PYTHONPATH"] = joined 60 | env[BuiltInEnvs.llm_model] = rt_param.model_name or "" 61 | env[BuiltInEnvs.parent_hash] = rt_param.parent_hash or "" 62 | env[BuiltInEnvs.context_contents] = "" 63 | if rt_param.history_messages: 64 | # convert dict to json string 65 | env[BuiltInEnvs.context_contents] = json.dumps(rt_param.history_messages) 66 | 67 | return env 68 | 69 | def _validate_and_interpolate( 70 | self, wf_config: WorkflowConfig, rt_param: RuntimeParameter 71 | ) -> List[str]: 72 | """ 73 | Validate the step configuration and interpolate variables in the command. 74 | 75 | Return the command parts as a list of strings. 76 | """ 77 | command_raw = self.command_raw 78 | parts = shlex.split(command_raw) 79 | 80 | args = [] 81 | for p in parts: 82 | arg = p 83 | 84 | if p.startswith(BuiltInVars.wf_python): 85 | if not rt_param.workflow_python: 86 | raise ValueError( 87 | f"The command uses {BuiltInVars.wf_python}, " 88 | "but the python path is not set yet." 89 | ) 90 | arg = arg.replace(BuiltInVars.wf_python, rt_param.workflow_python) 91 | 92 | if p.startswith(BuiltInVars.wf_dirpath): 93 | path_parts = p.split("/") 94 | # replace "$__idir__" with the root path in path_parts 95 | arg = os.path.join(wf_config.dirpath, *path_parts[1:]) 96 | 97 | if BuiltInVars.user_input in p: 98 | arg = arg.replace(BuiltInVars.user_input, rt_param.user_input) 99 | 100 | args.append(arg) 101 | 102 | return args 103 | 104 | def run(self, wf_config: WorkflowConfig, rt_param: RuntimeParameter) -> Tuple[int, str, str]: 105 | """ 106 | Run the step in a subprocess. 107 | 108 | Returns the return code, stdout, and stderr. 109 | """ 110 | # setup the environment variables 111 | env = self._setup_env(wf_config, rt_param) 112 | 113 | command_args = self._validate_and_interpolate(wf_config, rt_param) 114 | 115 | # NOTE: handle stdout & stderr if needed 116 | # def _pipe_reader(pipe, data, out_file): 117 | # """ 118 | # Read from the pipe, then write and save the data. 119 | # """ 120 | # while pipe: 121 | # pipe_data = pipe.read(1) 122 | # if pipe_data == "": 123 | # break 124 | # data["data"] += pipe_data 125 | # print(pipe_data, end="", file=out_file, flush=True) 126 | with subprocess.Popen( 127 | command_args, 128 | # stdout=subprocess.PIPE, 129 | # stderr=subprocess.PIPE, 130 | env=env, 131 | text=True, 132 | ) as proc: 133 | # stdout_data, stderr_data = {"data": ""}, {"data": ""} 134 | # stdout_thread = threading.Thread( 135 | # target=_pipe_reader, args=(proc.stdout, stdout_data, sys.stdout) 136 | # ) 137 | # stderr_thread = threading.Thread( 138 | # target=_pipe_reader, args=(proc.stderr, stderr_data, sys.stderr) 139 | # ) 140 | # stdout_thread.start() 141 | # stderr_thread.start() 142 | # stdout_thread.join() 143 | # stderr_thread.join() 144 | 145 | proc.wait() 146 | return_code = proc.returncode 147 | 148 | # return return_code, stdout_data["data"], stderr_data["data"] 149 | return return_code, "", "" 150 | -------------------------------------------------------------------------------- /devstream/user_setting.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | import oyaml as yaml 4 | 5 | from .path import USER_SETTINGS_FILE 6 | from .schema import UserSettings 7 | 8 | 9 | def _load_user_settings() -> UserSettings: 10 | """ 11 | Load the user settings from the settings.yml file. 12 | """ 13 | settings_path = Path(USER_SETTINGS_FILE) 14 | if not settings_path.exists(): 15 | return UserSettings() 16 | 17 | with open(settings_path, "r", encoding="utf-8") as file: 18 | content = yaml.safe_load(file.read()) 19 | 20 | if content: 21 | return UserSettings.model_validate(content) 22 | 23 | return UserSettings() 24 | 25 | 26 | USER_SETTINGS = _load_user_settings() 27 | -------------------------------------------------------------------------------- /devstream/utils.py: -------------------------------------------------------------------------------- 1 | import locale 2 | 3 | 4 | def get_sys_language_code(): 5 | """ 6 | Return the two-letter language code following ISO 639-1. 7 | """ 8 | lang, _ = locale.getdefaultlocale() 9 | return lang.split("_")[0].lower() 10 | -------------------------------------------------------------------------------- /devstream/workflow.py: -------------------------------------------------------------------------------- 1 | import logging 2 | import os 3 | import sys 4 | from typing import Dict, List, Optional, Tuple 5 | 6 | import oyaml as yaml 7 | 8 | from .env_manager import EXTERNAL_ENVS 9 | from .namespace import get_prioritized_namespace_path 10 | from .path import INTERFACE_FILENAMES 11 | from .schema import RuntimeParameter, WorkflowConfig 12 | from .step import WorkflowStep 13 | from .utils import get_sys_language_code 14 | 15 | logger = logging.getLogger(__name__) 16 | 17 | 18 | class Workflow: 19 | TRIGGER_PREFIX = "/" 20 | HELP_FLAG_PREFIX = "--help" 21 | 22 | def __init__(self, config: WorkflowConfig): 23 | self._config = config 24 | 25 | self._runtime_param = None 26 | 27 | @property 28 | def config(self) -> WorkflowConfig: 29 | return self._config 30 | 31 | @property 32 | def runtime_param(self): 33 | return self._runtime_param 34 | 35 | @staticmethod 36 | def parse_trigger(user_input: str) -> Tuple[Optional[str], Optional[str]]: 37 | """ 38 | Check if the user input should trigger a workflow. 39 | Return a tuple of (workflow_name, the input without workflow trigger). 40 | 41 | User input is considered a workflow trigger if it starts with the Workflow.PREFIX. 42 | The workflow name is the first word after the prefix. 43 | """ 44 | striped = user_input.strip() 45 | if not striped: 46 | return None, user_input 47 | if striped[0] != Workflow.TRIGGER_PREFIX: 48 | return None, user_input 49 | 50 | workflow_name = striped.split()[0][1:] 51 | 52 | # remove the trigger prefix and the workflow name 53 | actual_input = user_input.replace(f"{Workflow.TRIGGER_PREFIX}{workflow_name}", "", 1) 54 | return workflow_name, actual_input 55 | 56 | @staticmethod 57 | def load(workflow_name: str) -> Optional["Workflow"]: 58 | """ 59 | Load a workflow from the interface.yml by name. 60 | A workflow name is the relative path of interface.yml 61 | to the /workflows dir joined by "." 62 | e.g 63 | - "unit_tests": means the interface file of the workflow is unit_tests/interface.yml 64 | - "commit.en": means the interface file is commit/en/interface.yml 65 | - "pr.review.zh": means the interface file is pr/review/zh/interface.yml 66 | 67 | TODO: 单个路径组件合法的正则表达式为:`^[A-Za-z0-9_-]{1,255}$` 68 | """ 69 | path_parts = workflow_name.split(".") 70 | if len(path_parts) < 1: 71 | return None 72 | 73 | rel_path = os.path.join(*path_parts) 74 | 75 | found = False 76 | workflow_dir = "" 77 | prioritized_dirs = get_prioritized_namespace_path() 78 | for wf_dir in prioritized_dirs: 79 | for fn in INTERFACE_FILENAMES: 80 | yaml_file = os.path.join(wf_dir, rel_path, fn) 81 | if os.path.exists(yaml_file): 82 | workflow_dir = wf_dir 83 | found = True 84 | break 85 | if found: 86 | break 87 | if not found: 88 | return None 89 | 90 | # Load and override yaml conf in top-down order 91 | config_dict = {} 92 | for i in range(len(path_parts)): 93 | cur_path = os.path.join(workflow_dir, *path_parts[: i + 1]) 94 | for fn in INTERFACE_FILENAMES: 95 | cur_yaml = os.path.join(cur_path, fn) 96 | 97 | if os.path.exists(cur_yaml): 98 | with open(cur_yaml, "r", encoding="utf-8") as file: 99 | yaml_content = file.read() 100 | cur_conf = yaml.safe_load(yaml_content) 101 | cur_conf["dirpath"] = cur_path 102 | 103 | # convert relative path to absolute path for dependencies file 104 | if cur_conf.get("runtime", {}).get("requirements"): 105 | rel_dep = cur_conf["runtime"]["requirements"] 106 | abs_dep = os.path.join(cur_path, rel_dep) 107 | cur_conf["runtime"]["requirements"] = abs_dep 108 | 109 | config_dict.update(cur_conf) 110 | 111 | config = WorkflowConfig.model_validate(config_dict) 112 | 113 | return Workflow(config) 114 | 115 | def setup( 116 | self, 117 | user_input: Optional[str], 118 | model_name: Optional[str] = None, 119 | history_messages: Optional[List[Dict]] = None, 120 | parent_hash: Optional[str] = None, 121 | ): 122 | """ 123 | Setup the workflow with the runtime parameters and env variables. 124 | """ 125 | # NOTE: prepare an internal default python if needed 126 | default_python = sys.executable 127 | 128 | workflow_py = None 129 | if self.config.runtime.venv: 130 | env_name = self.config.runtime.venv 131 | if env_name in EXTERNAL_ENVS: 132 | # Use the external python set in the user settings 133 | workflow_py = EXTERNAL_ENVS[env_name].py_bin 134 | msg = [ 135 | "Using external Python from user settings:", 136 | f"- env_name: {env_name}", 137 | f"- python_bin: {workflow_py}", 138 | "This Python environment's version and dependencies should be " 139 | "ensured by the user to meet the requirements.", 140 | ] 141 | logger.debug("\n".join(msg)) 142 | 143 | else: 144 | # version = self.config.runtime.python_version 145 | # req_file = self.config.runtime.requirements 146 | # manager = PyEnvManager() 147 | # workflow_py = manager.ensure(env_name, version, req_file) 148 | pass 149 | else: 150 | pass 151 | 152 | workflow_py = workflow_py or default_python 153 | runtime_param = { 154 | # from user interaction 155 | "user_input": user_input, 156 | "model_name": model_name, 157 | "history_messages": history_messages, 158 | "parent_hash": parent_hash, 159 | # from user setting or system 160 | "workflow_python": workflow_py, 161 | } 162 | 163 | self._runtime_param = RuntimeParameter.model_validate(runtime_param) 164 | 165 | def run(self) -> int: 166 | """ 167 | Run the steps of the workflow. 168 | """ 169 | steps = self.config.runtime.run 170 | 171 | for s in steps: 172 | step = WorkflowStep(cmd=s) 173 | result = step.run(self.config, self.runtime_param) 174 | return_code = result[0] 175 | if return_code != 0: 176 | # stop the workflow if any step fails 177 | return return_code 178 | 179 | return 0 180 | 181 | def get_help_doc(self, user_input: str) -> Optional[str]: 182 | """ 183 | Get the help doc content of the workflow. 184 | """ 185 | help_info = self.config.help 186 | help_file = None 187 | 188 | if isinstance(help_info, str): 189 | # return the only help doc 190 | help_file = help_info 191 | 192 | elif isinstance(help_info, list) and len(help_info) > 0: 193 | first = help_info[0] 194 | assert isinstance(first, dict) 195 | default_file = list(first.values())[0] 196 | 197 | # get language code from user input 198 | code = user_input.strip().removeprefix(Workflow.HELP_FLAG_PREFIX) 199 | code = code.removeprefix(".").strip() 200 | code = code.lower() 201 | code = code or get_sys_language_code() 202 | help_info_dict = {list(d.keys())[0].lower(): list(d.values())[0] for d in help_info} 203 | help_file = help_info_dict.get(code, default_file) 204 | 205 | if not help_file: 206 | return None # or raise error? no help file configured 207 | 208 | help_path = os.path.join(self.config.dirpath, help_file) 209 | if os.path.exists(help_path): 210 | with open(help_path, "r", encoding="utf-8") as file: 211 | return file.read() 212 | 213 | return None # or raise error? help file not found 214 | 215 | def should_show_help(self, user_input) -> bool: 216 | return user_input.strip().startswith(Workflow.HELP_FLAG_PREFIX) 217 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "devstream" 3 | version = "0.1.0" 4 | description = "" 5 | readme = "README.md" 6 | requires-python = ">=3.10" 7 | dependencies = [ 8 | "oyaml>=1.0", 9 | "pydantic>=2.10.6", 10 | "virtualenv>=20.29.3", 11 | ] 12 | 13 | [build-system] 14 | requires = ["hatchling"] 15 | build-backend = "hatchling.build" 16 | 17 | [dependency-groups] 18 | dev = [ 19 | "python-dotenv>=1.0.1", 20 | "ruff>=0.9.10", 21 | ] 22 | 23 | 24 | [tool.ruff] 25 | target-version = "py310" 26 | line-length = 100 27 | 28 | [tool.ruff.lint] 29 | select = [ 30 | "E", # Error 31 | "W", # Warning 32 | "F", # pyflakes 33 | "I", # isort 34 | ] 35 | fixable = ["ALL"] 36 | -------------------------------------------------------------------------------- /specs/interface-v1.0.0.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devstream-io/devstream/24cdd2be1424d8ea7eb3a5e303585676f4363da7/specs/interface-v1.0.0.md -------------------------------------------------------------------------------- /specs/runtime-v1.0.0.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/devstream-io/devstream/24cdd2be1424d8ea7eb3a5e303585676f4363da7/specs/runtime-v1.0.0.md -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = ">=3.10" 4 | 5 | [[package]] 6 | name = "annotated-types" 7 | version = "0.7.0" 8 | source = { registry = "https://pypi.org/simple" } 9 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 10 | wheels = [ 11 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 12 | ] 13 | 14 | [[package]] 15 | name = "devstream" 16 | version = "0.1.0" 17 | source = { editable = "." } 18 | dependencies = [ 19 | { name = "oyaml" }, 20 | { name = "pydantic" }, 21 | { name = "virtualenv" }, 22 | ] 23 | 24 | [package.dev-dependencies] 25 | dev = [ 26 | { name = "python-dotenv" }, 27 | { name = "ruff" }, 28 | ] 29 | 30 | [package.metadata] 31 | requires-dist = [ 32 | { name = "oyaml", specifier = ">=1.0" }, 33 | { name = "pydantic", specifier = ">=2.10.6" }, 34 | { name = "virtualenv", specifier = ">=20.29.3" }, 35 | ] 36 | 37 | [package.metadata.requires-dev] 38 | dev = [ 39 | { name = "python-dotenv", specifier = ">=1.0.1" }, 40 | { name = "ruff", specifier = ">=0.9.10" }, 41 | ] 42 | 43 | [[package]] 44 | name = "distlib" 45 | version = "0.3.9" 46 | source = { registry = "https://pypi.org/simple" } 47 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } 48 | wheels = [ 49 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, 50 | ] 51 | 52 | [[package]] 53 | name = "filelock" 54 | version = "3.17.0" 55 | source = { registry = "https://pypi.org/simple" } 56 | sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } 57 | wheels = [ 58 | { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, 59 | ] 60 | 61 | [[package]] 62 | name = "oyaml" 63 | version = "1.0" 64 | source = { registry = "https://pypi.org/simple" } 65 | dependencies = [ 66 | { name = "pyyaml" }, 67 | ] 68 | sdist = { url = "https://files.pythonhosted.org/packages/00/71/c721b9a524f6fe6f73469c90ec44784f0b2b1b23c438da7cc7daac1ede76/oyaml-1.0.tar.gz", hash = "sha256:ed8fc096811f4763e1907dce29c35895d6d5936c4d0400fe843a91133d4744ed", size = 2914 } 69 | wheels = [ 70 | { url = "https://files.pythonhosted.org/packages/37/aa/111610d8bf5b1bb7a295a048fc648cec346347a8b0be5881defd2d1b4a52/oyaml-1.0-py2.py3-none-any.whl", hash = "sha256:3a378747b7fb2425533d1ce41962d6921cda075d46bb480a158d45242d156323", size = 2992 }, 71 | ] 72 | 73 | [[package]] 74 | name = "platformdirs" 75 | version = "4.3.6" 76 | source = { registry = "https://pypi.org/simple" } 77 | sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } 78 | wheels = [ 79 | { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, 80 | ] 81 | 82 | [[package]] 83 | name = "pydantic" 84 | version = "2.10.6" 85 | source = { registry = "https://pypi.org/simple" } 86 | dependencies = [ 87 | { name = "annotated-types" }, 88 | { name = "pydantic-core" }, 89 | { name = "typing-extensions" }, 90 | ] 91 | sdist = { url = "https://files.pythonhosted.org/packages/b7/ae/d5220c5c52b158b1de7ca89fc5edb72f304a70a4c540c84c8844bf4008de/pydantic-2.10.6.tar.gz", hash = "sha256:ca5daa827cce33de7a42be142548b0096bf05a7e7b365aebfa5f8eeec7128236", size = 761681 } 92 | wheels = [ 93 | { url = "https://files.pythonhosted.org/packages/f4/3c/8cc1cc84deffa6e25d2d0c688ebb80635dfdbf1dbea3e30c541c8cf4d860/pydantic-2.10.6-py3-none-any.whl", hash = "sha256:427d664bf0b8a2b34ff5dd0f5a18df00591adcee7198fbd71981054cef37b584", size = 431696 }, 94 | ] 95 | 96 | [[package]] 97 | name = "pydantic-core" 98 | version = "2.27.2" 99 | source = { registry = "https://pypi.org/simple" } 100 | dependencies = [ 101 | { name = "typing-extensions" }, 102 | ] 103 | sdist = { url = "https://files.pythonhosted.org/packages/fc/01/f3e5ac5e7c25833db5eb555f7b7ab24cd6f8c322d3a3ad2d67a952dc0abc/pydantic_core-2.27.2.tar.gz", hash = "sha256:eb026e5a4c1fee05726072337ff51d1efb6f59090b7da90d30ea58625b1ffb39", size = 413443 } 104 | wheels = [ 105 | { url = "https://files.pythonhosted.org/packages/3a/bc/fed5f74b5d802cf9a03e83f60f18864e90e3aed7223adaca5ffb7a8d8d64/pydantic_core-2.27.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:2d367ca20b2f14095a8f4fa1210f5a7b78b8a20009ecced6b12818f455b1e9fa", size = 1895938 }, 106 | { url = "https://files.pythonhosted.org/packages/71/2a/185aff24ce844e39abb8dd680f4e959f0006944f4a8a0ea372d9f9ae2e53/pydantic_core-2.27.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:491a2b73db93fab69731eaee494f320faa4e093dbed776be1a829c2eb222c34c", size = 1815684 }, 107 | { url = "https://files.pythonhosted.org/packages/c3/43/fafabd3d94d159d4f1ed62e383e264f146a17dd4d48453319fd782e7979e/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7969e133a6f183be60e9f6f56bfae753585680f3b7307a8e555a948d443cc05a", size = 1829169 }, 108 | { url = "https://files.pythonhosted.org/packages/a2/d1/f2dfe1a2a637ce6800b799aa086d079998959f6f1215eb4497966efd2274/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3de9961f2a346257caf0aa508a4da705467f53778e9ef6fe744c038119737ef5", size = 1867227 }, 109 | { url = "https://files.pythonhosted.org/packages/7d/39/e06fcbcc1c785daa3160ccf6c1c38fea31f5754b756e34b65f74e99780b5/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e2bb4d3e5873c37bb3dd58714d4cd0b0e6238cebc4177ac8fe878f8b3aa8e74c", size = 2037695 }, 110 | { url = "https://files.pythonhosted.org/packages/7a/67/61291ee98e07f0650eb756d44998214231f50751ba7e13f4f325d95249ab/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:280d219beebb0752699480fe8f1dc61ab6615c2046d76b7ab7ee38858de0a4e7", size = 2741662 }, 111 | { url = "https://files.pythonhosted.org/packages/32/90/3b15e31b88ca39e9e626630b4c4a1f5a0dfd09076366f4219429e6786076/pydantic_core-2.27.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:47956ae78b6422cbd46f772f1746799cbb862de838fd8d1fbd34a82e05b0983a", size = 1993370 }, 112 | { url = "https://files.pythonhosted.org/packages/ff/83/c06d333ee3a67e2e13e07794995c1535565132940715931c1c43bfc85b11/pydantic_core-2.27.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:14d4a5c49d2f009d62a2a7140d3064f686d17a5d1a268bc641954ba181880236", size = 1996813 }, 113 | { url = "https://files.pythonhosted.org/packages/7c/f7/89be1c8deb6e22618a74f0ca0d933fdcb8baa254753b26b25ad3acff8f74/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:337b443af21d488716f8d0b6164de833e788aa6bd7e3a39c005febc1284f4962", size = 2005287 }, 114 | { url = "https://files.pythonhosted.org/packages/b7/7d/8eb3e23206c00ef7feee17b83a4ffa0a623eb1a9d382e56e4aa46fd15ff2/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:03d0f86ea3184a12f41a2d23f7ccb79cdb5a18e06993f8a45baa8dfec746f0e9", size = 2128414 }, 115 | { url = "https://files.pythonhosted.org/packages/4e/99/fe80f3ff8dd71a3ea15763878d464476e6cb0a2db95ff1c5c554133b6b83/pydantic_core-2.27.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:7041c36f5680c6e0f08d922aed302e98b3745d97fe1589db0a3eebf6624523af", size = 2155301 }, 116 | { url = "https://files.pythonhosted.org/packages/2b/a3/e50460b9a5789ca1451b70d4f52546fa9e2b420ba3bfa6100105c0559238/pydantic_core-2.27.2-cp310-cp310-win32.whl", hash = "sha256:50a68f3e3819077be2c98110c1f9dcb3817e93f267ba80a2c05bb4f8799e2ff4", size = 1816685 }, 117 | { url = "https://files.pythonhosted.org/packages/57/4c/a8838731cb0f2c2a39d3535376466de6049034d7b239c0202a64aaa05533/pydantic_core-2.27.2-cp310-cp310-win_amd64.whl", hash = "sha256:e0fd26b16394ead34a424eecf8a31a1f5137094cabe84a1bcb10fa6ba39d3d31", size = 1982876 }, 118 | { url = "https://files.pythonhosted.org/packages/c2/89/f3450af9d09d44eea1f2c369f49e8f181d742f28220f88cc4dfaae91ea6e/pydantic_core-2.27.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:8e10c99ef58cfdf2a66fc15d66b16c4a04f62bca39db589ae8cba08bc55331bc", size = 1893421 }, 119 | { url = "https://files.pythonhosted.org/packages/9e/e3/71fe85af2021f3f386da42d291412e5baf6ce7716bd7101ea49c810eda90/pydantic_core-2.27.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:26f32e0adf166a84d0cb63be85c562ca8a6fa8de28e5f0d92250c6b7e9e2aff7", size = 1814998 }, 120 | { url = "https://files.pythonhosted.org/packages/a6/3c/724039e0d848fd69dbf5806894e26479577316c6f0f112bacaf67aa889ac/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c19d1ea0673cd13cc2f872f6c9ab42acc4e4f492a7ca9d3795ce2b112dd7e15", size = 1826167 }, 121 | { url = "https://files.pythonhosted.org/packages/2b/5b/1b29e8c1fb5f3199a9a57c1452004ff39f494bbe9bdbe9a81e18172e40d3/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5e68c4446fe0810e959cdff46ab0a41ce2f2c86d227d96dc3847af0ba7def306", size = 1865071 }, 122 | { url = "https://files.pythonhosted.org/packages/89/6c/3985203863d76bb7d7266e36970d7e3b6385148c18a68cc8915fd8c84d57/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d9640b0059ff4f14d1f37321b94061c6db164fbe49b334b31643e0528d100d99", size = 2036244 }, 123 | { url = "https://files.pythonhosted.org/packages/0e/41/f15316858a246b5d723f7d7f599f79e37493b2e84bfc789e58d88c209f8a/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:40d02e7d45c9f8af700f3452f329ead92da4c5f4317ca9b896de7ce7199ea459", size = 2737470 }, 124 | { url = "https://files.pythonhosted.org/packages/a8/7c/b860618c25678bbd6d1d99dbdfdf0510ccb50790099b963ff78a124b754f/pydantic_core-2.27.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1c1fd185014191700554795c99b347d64f2bb637966c4cfc16998a0ca700d048", size = 1992291 }, 125 | { url = "https://files.pythonhosted.org/packages/bf/73/42c3742a391eccbeab39f15213ecda3104ae8682ba3c0c28069fbcb8c10d/pydantic_core-2.27.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d81d2068e1c1228a565af076598f9e7451712700b673de8f502f0334f281387d", size = 1994613 }, 126 | { url = "https://files.pythonhosted.org/packages/94/7a/941e89096d1175d56f59340f3a8ebaf20762fef222c298ea96d36a6328c5/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:1a4207639fb02ec2dbb76227d7c751a20b1a6b4bc52850568e52260cae64ca3b", size = 2002355 }, 127 | { url = "https://files.pythonhosted.org/packages/6e/95/2359937a73d49e336a5a19848713555605d4d8d6940c3ec6c6c0ca4dcf25/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:3de3ce3c9ddc8bbd88f6e0e304dea0e66d843ec9de1b0042b0911c1663ffd474", size = 2126661 }, 128 | { url = "https://files.pythonhosted.org/packages/2b/4c/ca02b7bdb6012a1adef21a50625b14f43ed4d11f1fc237f9d7490aa5078c/pydantic_core-2.27.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:30c5f68ded0c36466acede341551106821043e9afaad516adfb6e8fa80a4e6a6", size = 2153261 }, 129 | { url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361 }, 130 | { url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484 }, 131 | { url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102 }, 132 | { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127 }, 133 | { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340 }, 134 | { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900 }, 135 | { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177 }, 136 | { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046 }, 137 | { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386 }, 138 | { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060 }, 139 | { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870 }, 140 | { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822 }, 141 | { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364 }, 142 | { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303 }, 143 | { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064 }, 144 | { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046 }, 145 | { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092 }, 146 | { url = "https://files.pythonhosted.org/packages/41/b1/9bc383f48f8002f99104e3acff6cba1231b29ef76cfa45d1506a5cad1f84/pydantic_core-2.27.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7d14bd329640e63852364c306f4d23eb744e0f8193148d4044dd3dacdaacbd8b", size = 1892709 }, 147 | { url = "https://files.pythonhosted.org/packages/10/6c/e62b8657b834f3eb2961b49ec8e301eb99946245e70bf42c8817350cbefc/pydantic_core-2.27.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82f91663004eb8ed30ff478d77c4d1179b3563df6cdb15c0817cd1cdaf34d154", size = 1811273 }, 148 | { url = "https://files.pythonhosted.org/packages/ba/15/52cfe49c8c986e081b863b102d6b859d9defc63446b642ccbbb3742bf371/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71b24c7d61131bb83df10cc7e687433609963a944ccf45190cfc21e0887b08c9", size = 1823027 }, 149 | { url = "https://files.pythonhosted.org/packages/b1/1c/b6f402cfc18ec0024120602bdbcebc7bdd5b856528c013bd4d13865ca473/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fa8e459d4954f608fa26116118bb67f56b93b209c39b008277ace29937453dc9", size = 1868888 }, 150 | { url = "https://files.pythonhosted.org/packages/bd/7b/8cb75b66ac37bc2975a3b7de99f3c6f355fcc4d89820b61dffa8f1e81677/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ce8918cbebc8da707ba805b7fd0b382816858728ae7fe19a942080c24e5b7cd1", size = 2037738 }, 151 | { url = "https://files.pythonhosted.org/packages/c8/f1/786d8fe78970a06f61df22cba58e365ce304bf9b9f46cc71c8c424e0c334/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:eda3f5c2a021bbc5d976107bb302e0131351c2ba54343f8a496dc8783d3d3a6a", size = 2685138 }, 152 | { url = "https://files.pythonhosted.org/packages/a6/74/d12b2cd841d8724dc8ffb13fc5cef86566a53ed358103150209ecd5d1999/pydantic_core-2.27.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bd8086fa684c4775c27f03f062cbb9eaa6e17f064307e86b21b9e0abc9c0f02e", size = 1997025 }, 153 | { url = "https://files.pythonhosted.org/packages/a0/6e/940bcd631bc4d9a06c9539b51f070b66e8f370ed0933f392db6ff350d873/pydantic_core-2.27.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:8d9b3388db186ba0c099a6d20f0604a44eabdeef1777ddd94786cdae158729e4", size = 2004633 }, 154 | { url = "https://files.pythonhosted.org/packages/50/cc/a46b34f1708d82498c227d5d80ce615b2dd502ddcfd8376fc14a36655af1/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7a66efda2387de898c8f38c0cf7f14fca0b51a8ef0b24bfea5849f1b3c95af27", size = 1999404 }, 155 | { url = "https://files.pythonhosted.org/packages/ca/2d/c365cfa930ed23bc58c41463bae347d1005537dc8db79e998af8ba28d35e/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:18a101c168e4e092ab40dbc2503bdc0f62010e95d292b27827871dc85450d7ee", size = 2130130 }, 156 | { url = "https://files.pythonhosted.org/packages/f4/d7/eb64d015c350b7cdb371145b54d96c919d4db516817f31cd1c650cae3b21/pydantic_core-2.27.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ba5dd002f88b78a4215ed2f8ddbdf85e8513382820ba15ad5ad8955ce0ca19a1", size = 2157946 }, 157 | { url = "https://files.pythonhosted.org/packages/a4/99/bddde3ddde76c03b65dfd5a66ab436c4e58ffc42927d4ff1198ffbf96f5f/pydantic_core-2.27.2-cp313-cp313-win32.whl", hash = "sha256:1ebaf1d0481914d004a573394f4be3a7616334be70261007e47c2a6fe7e50130", size = 1834387 }, 158 | { url = "https://files.pythonhosted.org/packages/71/47/82b5e846e01b26ac6f1893d3c5f9f3a2eb6ba79be26eef0b759b4fe72946/pydantic_core-2.27.2-cp313-cp313-win_amd64.whl", hash = "sha256:953101387ecf2f5652883208769a79e48db18c6df442568a0b5ccd8c2723abee", size = 1990453 }, 159 | { url = "https://files.pythonhosted.org/packages/51/b2/b2b50d5ecf21acf870190ae5d093602d95f66c9c31f9d5de6062eb329ad1/pydantic_core-2.27.2-cp313-cp313-win_arm64.whl", hash = "sha256:ac4dbfd1691affb8f48c2c13241a2e3b60ff23247cbcf981759c768b6633cf8b", size = 1885186 }, 160 | { url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159 }, 161 | { url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331 }, 162 | { url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467 }, 163 | { url = "https://files.pythonhosted.org/packages/d7/7a/7bbf241a04e9f9ea24cd5874354a83526d639b02674648af3f350554276c/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d591580c34f4d731592f0e9fe40f9cc1b430d297eecc70b962e93c5c668f15f", size = 1979797 }, 164 | { url = "https://files.pythonhosted.org/packages/4f/5f/4784c6107731f89e0005a92ecb8a2efeafdb55eb992b8e9d0a2be5199335/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:82f986faf4e644ffc189a7f1aafc86e46ef70372bb153e7001e8afccc6e54133", size = 1987839 }, 165 | { url = "https://files.pythonhosted.org/packages/6d/a7/61246562b651dff00de86a5f01b6e4befb518df314c54dec187a78d81c84/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:bec317a27290e2537f922639cafd54990551725fc844249e64c523301d0822fc", size = 1998861 }, 166 | { url = "https://files.pythonhosted.org/packages/86/aa/837821ecf0c022bbb74ca132e117c358321e72e7f9702d1b6a03758545e2/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:0296abcb83a797db256b773f45773da397da75a08f5fcaef41f2044adec05f50", size = 2116582 }, 167 | { url = "https://files.pythonhosted.org/packages/81/b0/5e74656e95623cbaa0a6278d16cf15e10a51f6002e3ec126541e95c29ea3/pydantic_core-2.27.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:0d75070718e369e452075a6017fbf187f788e17ed67a3abd47fa934d001863d9", size = 2151985 }, 168 | { url = "https://files.pythonhosted.org/packages/63/37/3e32eeb2a451fddaa3898e2163746b0cffbbdbb4740d38372db0490d67f3/pydantic_core-2.27.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:7e17b560be3c98a8e3aa66ce828bdebb9e9ac6ad5466fba92eb74c4c95cb1151", size = 2004715 }, 169 | ] 170 | 171 | [[package]] 172 | name = "python-dotenv" 173 | version = "1.0.1" 174 | source = { registry = "https://pypi.org/simple" } 175 | sdist = { url = "https://files.pythonhosted.org/packages/bc/57/e84d88dfe0aec03b7a2d4327012c1627ab5f03652216c63d49846d7a6c58/python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca", size = 39115 } 176 | wheels = [ 177 | { url = "https://files.pythonhosted.org/packages/6a/3e/b68c118422ec867fa7ab88444e1274aa40681c606d59ac27de5a5588f082/python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a", size = 19863 }, 178 | ] 179 | 180 | [[package]] 181 | name = "pyyaml" 182 | version = "6.0.2" 183 | source = { registry = "https://pypi.org/simple" } 184 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 185 | wheels = [ 186 | { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199 }, 187 | { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758 }, 188 | { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463 }, 189 | { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280 }, 190 | { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239 }, 191 | { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802 }, 192 | { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527 }, 193 | { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052 }, 194 | { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774 }, 195 | { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612 }, 196 | { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040 }, 197 | { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829 }, 198 | { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167 }, 199 | { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952 }, 200 | { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301 }, 201 | { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638 }, 202 | { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850 }, 203 | { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980 }, 204 | { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873 }, 205 | { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302 }, 206 | { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154 }, 207 | { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223 }, 208 | { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542 }, 209 | { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164 }, 210 | { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611 }, 211 | { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591 }, 212 | { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338 }, 213 | { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309 }, 214 | { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679 }, 215 | { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428 }, 216 | { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361 }, 217 | { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523 }, 218 | { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660 }, 219 | { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597 }, 220 | { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527 }, 221 | { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446 }, 222 | ] 223 | 224 | [[package]] 225 | name = "ruff" 226 | version = "0.9.10" 227 | source = { registry = "https://pypi.org/simple" } 228 | sdist = { url = "https://files.pythonhosted.org/packages/20/8e/fafaa6f15c332e73425d9c44ada85360501045d5ab0b81400076aff27cf6/ruff-0.9.10.tar.gz", hash = "sha256:9bacb735d7bada9cfb0f2c227d3658fc443d90a727b47f206fb33f52f3c0eac7", size = 3759776 } 229 | wheels = [ 230 | { url = "https://files.pythonhosted.org/packages/73/b2/af7c2cc9e438cbc19fafeec4f20bfcd72165460fe75b2b6e9a0958c8c62b/ruff-0.9.10-py3-none-linux_armv6l.whl", hash = "sha256:eb4d25532cfd9fe461acc83498361ec2e2252795b4f40b17e80692814329e42d", size = 10049494 }, 231 | { url = "https://files.pythonhosted.org/packages/6d/12/03f6dfa1b95ddd47e6969f0225d60d9d7437c91938a310835feb27927ca0/ruff-0.9.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:188a6638dab1aa9bb6228a7302387b2c9954e455fb25d6b4470cb0641d16759d", size = 10853584 }, 232 | { url = "https://files.pythonhosted.org/packages/02/49/1c79e0906b6ff551fb0894168763f705bf980864739572b2815ecd3c9df0/ruff-0.9.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5284dcac6b9dbc2fcb71fdfc26a217b2ca4ede6ccd57476f52a587451ebe450d", size = 10155692 }, 233 | { url = "https://files.pythonhosted.org/packages/5b/01/85e8082e41585e0e1ceb11e41c054e9e36fed45f4b210991052d8a75089f/ruff-0.9.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47678f39fa2a3da62724851107f438c8229a3470f533894b5568a39b40029c0c", size = 10369760 }, 234 | { url = "https://files.pythonhosted.org/packages/a1/90/0bc60bd4e5db051f12445046d0c85cc2c617095c0904f1aa81067dc64aea/ruff-0.9.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99713a6e2766b7a17147b309e8c915b32b07a25c9efd12ada79f217c9c778b3e", size = 9912196 }, 235 | { url = "https://files.pythonhosted.org/packages/66/ea/0b7e8c42b1ec608033c4d5a02939c82097ddcb0b3e393e4238584b7054ab/ruff-0.9.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:524ee184d92f7c7304aa568e2db20f50c32d1d0caa235d8ddf10497566ea1a12", size = 11434985 }, 236 | { url = "https://files.pythonhosted.org/packages/d5/86/3171d1eff893db4f91755175a6e1163c5887be1f1e2f4f6c0c59527c2bfd/ruff-0.9.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:df92aeac30af821f9acf819fc01b4afc3dfb829d2782884f8739fb52a8119a16", size = 12155842 }, 237 | { url = "https://files.pythonhosted.org/packages/89/9e/700ca289f172a38eb0bca752056d0a42637fa17b81649b9331786cb791d7/ruff-0.9.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de42e4edc296f520bb84954eb992a07a0ec5a02fecb834498415908469854a52", size = 11613804 }, 238 | { url = "https://files.pythonhosted.org/packages/f2/92/648020b3b5db180f41a931a68b1c8575cca3e63cec86fd26807422a0dbad/ruff-0.9.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d257f95b65806104b6b1ffca0ea53f4ef98454036df65b1eda3693534813ecd1", size = 13823776 }, 239 | { url = "https://files.pythonhosted.org/packages/5e/a6/cc472161cd04d30a09d5c90698696b70c169eeba2c41030344194242db45/ruff-0.9.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b60dec7201c0b10d6d11be00e8f2dbb6f40ef1828ee75ed739923799513db24c", size = 11302673 }, 240 | { url = "https://files.pythonhosted.org/packages/6c/db/d31c361c4025b1b9102b4d032c70a69adb9ee6fde093f6c3bf29f831c85c/ruff-0.9.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d838b60007da7a39c046fcdd317293d10b845001f38bcb55ba766c3875b01e43", size = 10235358 }, 241 | { url = "https://files.pythonhosted.org/packages/d1/86/d6374e24a14d4d93ebe120f45edd82ad7dcf3ef999ffc92b197d81cdc2a5/ruff-0.9.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ccaf903108b899beb8e09a63ffae5869057ab649c1e9231c05ae354ebc62066c", size = 9886177 }, 242 | { url = "https://files.pythonhosted.org/packages/00/62/a61691f6eaaac1e945a1f3f59f1eea9a218513139d5b6c2b8f88b43b5b8f/ruff-0.9.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f9567d135265d46e59d62dc60c0bfad10e9a6822e231f5b24032dba5a55be6b5", size = 10864747 }, 243 | { url = "https://files.pythonhosted.org/packages/ee/94/2c7065e1d92a8a8a46d46d9c3cf07b0aa7e0a1e0153d74baa5e6620b4102/ruff-0.9.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:5f202f0d93738c28a89f8ed9eaba01b7be339e5d8d642c994347eaa81c6d75b8", size = 11360441 }, 244 | { url = "https://files.pythonhosted.org/packages/a7/8f/1f545ea6f9fcd7bf4368551fb91d2064d8f0577b3079bb3f0ae5779fb773/ruff-0.9.10-py3-none-win32.whl", hash = "sha256:bfb834e87c916521ce46b1788fbb8484966e5113c02df216680102e9eb960029", size = 10247401 }, 245 | { url = "https://files.pythonhosted.org/packages/4f/18/fb703603ab108e5c165f52f5b86ee2aa9be43bb781703ec87c66a5f5d604/ruff-0.9.10-py3-none-win_amd64.whl", hash = "sha256:f2160eeef3031bf4b17df74e307d4c5fb689a6f3a26a2de3f7ef4044e3c484f1", size = 11366360 }, 246 | { url = "https://files.pythonhosted.org/packages/35/85/338e603dc68e7d9994d5d84f24adbf69bae760ba5efd3e20f5ff2cec18da/ruff-0.9.10-py3-none-win_arm64.whl", hash = "sha256:5fd804c0327a5e5ea26615550e706942f348b197d5475ff34c19733aee4b2e69", size = 10436892 }, 247 | ] 248 | 249 | [[package]] 250 | name = "typing-extensions" 251 | version = "4.12.2" 252 | source = { registry = "https://pypi.org/simple" } 253 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 254 | wheels = [ 255 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 256 | ] 257 | 258 | [[package]] 259 | name = "virtualenv" 260 | version = "20.29.3" 261 | source = { registry = "https://pypi.org/simple" } 262 | dependencies = [ 263 | { name = "distlib" }, 264 | { name = "filelock" }, 265 | { name = "platformdirs" }, 266 | ] 267 | sdist = { url = "https://files.pythonhosted.org/packages/c7/9c/57d19fa093bcf5ac61a48087dd44d00655f85421d1aa9722f8befbf3f40a/virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac", size = 4320280 } 268 | wheels = [ 269 | { url = "https://files.pythonhosted.org/packages/c2/eb/c6db6e3001d58c6a9e67c74bb7b4206767caa3ccc28c6b9eaf4c23fb4e34/virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", size = 4301458 }, 270 | ] 271 | --------------------------------------------------------------------------------