├── .gitignore ├── LICENSE ├── README.md ├── poetry.lock ├── pyproject.toml ├── pytest.ini ├── requirements.txt └── src └── batchwizard ├── __init__.py ├── cli.py ├── config.py ├── models.py ├── processor.py ├── ui.py └── utils.py /.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 | # poetry 98 | # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. 99 | # This is especially recommended for binary packages to ensure reproducibility, and is more 100 | # commonly ignored for libraries. 101 | # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control 102 | #poetry.lock 103 | 104 | # pdm 105 | # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. 106 | #pdm.lock 107 | # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it 108 | # in version control. 109 | # https://pdm.fming.dev/latest/usage/project/#working-with-version-control 110 | .pdm.toml 111 | .pdm-python 112 | .pdm-build/ 113 | 114 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm 115 | __pypackages__/ 116 | 117 | # Celery stuff 118 | celerybeat-schedule 119 | celerybeat.pid 120 | 121 | # SageMath parsed files 122 | *.sage.py 123 | 124 | # Environments 125 | .env 126 | .venv 127 | env/ 128 | venv/ 129 | ENV/ 130 | env.bak/ 131 | venv.bak/ 132 | 133 | # Spyder project settings 134 | .spyderproject 135 | .spyproject 136 | 137 | # Rope project settings 138 | .ropeproject 139 | 140 | # mkdocs documentation 141 | /site 142 | 143 | # mypy 144 | .mypy_cache/ 145 | .dmypy.json 146 | dmypy.json 147 | 148 | # Pyre type checker 149 | .pyre/ 150 | 151 | # pytype static type analyzer 152 | .pytype/ 153 | 154 | # Cython debug symbols 155 | cython_debug/ 156 | 157 | # PyCharm 158 | # JetBrains specific template is maintained in a separate JetBrains.gitignore that can 159 | # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore 160 | # and can be added to the global gitignore or merged into this file. For a more nuclear 161 | # option (not recommended) you can uncomment the following to ignore the entire idea folder. 162 | #.idea/ 163 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Carl Kugblenu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # BatchWizard 2 | 3 | BatchWizard is a powerful CLI tool for managing OpenAI batch processing jobs with ease. It provides functionalities to upload files, create batch jobs, check their status, and download the results. The tool uses asynchronous processing to efficiently handle multiple jobs concurrently. 4 | 5 | ![image](https://github.com/user-attachments/assets/8084afbd-fd05-43b3-b57c-2ea1eb70a457) 6 | 7 | ## Table of Contents 8 | 9 | - [Installation](#installation) 10 | - [Usage](#usage) 11 | - [Configuration](#configuration) 12 | - [Commands](#commands) 13 | - [Features](#features) 14 | - [Contributing](#contributing) 15 | - [License](#license) 16 | 17 | ## Installation 18 | 19 | You can install BatchWizard using `pipx` for an isolated environment or directly via `pip`. 20 | 21 | ### Using pipx (recommended) 22 | 23 | ```bash 24 | pipx install batchwizard 25 | ``` 26 | 27 | ### Using pip 28 | 29 | ```bash 30 | pip install batchwizard 31 | ``` 32 | 33 | Ensure you have `pipx` or `pip` installed on your system. For `pipx`, you can follow the installation instructions [here](https://pipx.pypa.io/stable/installation/). 34 | 35 | ## Usage 36 | 37 | BatchWizard provides a command-line interface (CLI) for managing batch jobs. Here are some example commands: 38 | 39 | ### Process Batch Jobs 40 | 41 | To process input files or directories: 42 | 43 | ```bash 44 | batchwizard process ... [--output-directory OUTPUT_DIR] [--max-concurrent-jobs NUM] [--check-interval SECONDS] 45 | ``` 46 | 47 | You can provide multiple input paths, which can be individual JSONL files or directories containing JSONL files. 48 | 49 | #### Example with Sample Input 50 | 51 | Let's say you have a file named `batchinput.jsonl` with the following content: 52 | 53 | ```jsonl 54 | {"custom_id": "request-1", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "system", "content": "You are a helpful assistant."},{"role": "user", "content": "Hello world!"}],"max_tokens": 1000}} 55 | {"custom_id": "request-2", "method": "POST", "url": "/v1/chat/completions", "body": {"model": "gpt-4o-mini", "messages": [{"role": "system", "content": "You are an unhelpful assistant."},{"role": "user", "content": "Hello world!"}],"max_tokens": 1000}} 56 | ``` 57 | 58 | To process this file using BatchWizard: 59 | 60 | 1. First, ensure your OpenAI API key is set: 61 | ```bash 62 | batchwizard configure --set-key YOUR_API_KEY 63 | ``` 64 | 2. Then, run the process command: 65 | ```bash 66 | batchwizard process /path/to/batchinput.jsonl --output-directory /path/to/output 67 | ``` 68 | This command will: 69 | - Upload the `batchinput.jsonl` file to OpenAI 70 | - Create a batch job 71 | - Monitor the job status 72 | - Download the results to the specified output directory when complete 73 | 74 | You can also process multiple files or directories: 75 | 76 | ```bash 77 | batchwizard process /path/to/file1.jsonl /path/to/directory_with_jsonl_files /path/to/file2.jsonl 78 | ``` 79 | 80 | ### List Recent Jobs 81 | 82 | To list recent batch jobs: 83 | 84 | ```bash 85 | batchwizard list-jobs [--limit NUM] [--all] 86 | ``` 87 | 88 | ### Cancel a Job 89 | 90 | To cancel a specific batch job: 91 | 92 | ```bash 93 | batchwizard cancel 94 | ``` 95 | 96 | ### Download Job Results 97 | 98 | To download results for a completed batch job: 99 | 100 | ```bash 101 | batchwizard download [--output-file FILE_PATH] 102 | ``` 103 | 104 | ## Configuration 105 | 106 | ### Setting up the OpenAI API Key 107 | 108 | To set the OpenAI API key: 109 | 110 | ```bash 111 | batchwizard configure --set-key YOUR_API_KEY 112 | ``` 113 | 114 | ### Show Current Configuration 115 | 116 | To show the current configuration: 117 | 118 | ```bash 119 | batchwizard configure --show 120 | ``` 121 | 122 | ### Reset Configuration 123 | 124 | To reset the configuration to default values: 125 | 126 | ```bash 127 | batchwizard configure --reset 128 | ``` 129 | 130 | ## Commands 131 | 132 | BatchWizard supports the following commands: 133 | 134 | - `process`: Process batch jobs from input files or directories. 135 | - `configure`: Manage BatchWizard configuration. 136 | - `list-jobs`: List recent batch jobs. 137 | - `cancel`: Cancel a specific batch job. 138 | - `download`: Download results for a completed batch job. 139 | 140 | For detailed information on each command, use the `--help` option: 141 | 142 | ```bash 143 | batchwizard --help 144 | ``` 145 | 146 | ## Features 147 | 148 | - **Flexible Input**: Process individual JSONL files or entire directories containing JSONL files. 149 | - **Asynchronous Processing**: Efficiently handle multiple batch jobs concurrently. 150 | - **Rich UI**: Display progress and job status using a rich, interactive interface. 151 | - **Flexible Configuration**: Easily manage API keys and other settings. 152 | - **Job Management**: List, cancel, and download results for batch jobs. 153 | - **Error Handling**: Robust error handling and informative error messages. 154 | 155 | ## Contributing 156 | 157 | We welcome contributions to BatchWizard! To contribute, follow these steps: 158 | 159 | 1. Fork the repository. 160 | 2. Create a new branch: `git checkout -b feature/your-feature-name`. 161 | 3. Make your changes and commit them: `git commit -m 'Add some feature'`. 162 | 4. Push to the branch: `git push origin feature/your-feature-name`. 163 | 5. Open a pull request. 164 | 165 | ### Running Tests 166 | 167 | To run tests, use `pytest`: 168 | 169 | ```bash 170 | pytest --cov=batchwizard tests/ 171 | ``` 172 | 173 | Ensure your code passes all tests and meets the coding standards before opening a pull request. 174 | 175 | ## License 176 | 177 | This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details. 178 | 179 | ## Contact 180 | 181 | For any questions or feedback, feel free to open an issue on the [GitHub repository](https://github.com/cmakafui/batchwizard). 182 | -------------------------------------------------------------------------------- /poetry.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. 2 | 3 | [[package]] 4 | name = "aiofiles" 5 | version = "24.1.0" 6 | description = "File support for asyncio." 7 | optional = false 8 | python-versions = ">=3.8" 9 | files = [ 10 | {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, 11 | {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, 12 | ] 13 | 14 | [[package]] 15 | name = "annotated-types" 16 | version = "0.7.0" 17 | description = "Reusable constraint types to use with typing.Annotated" 18 | optional = false 19 | python-versions = ">=3.8" 20 | files = [ 21 | {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, 22 | {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, 23 | ] 24 | 25 | [[package]] 26 | name = "anyio" 27 | version = "4.4.0" 28 | description = "High level compatibility layer for multiple asynchronous event loop implementations" 29 | optional = false 30 | python-versions = ">=3.8" 31 | files = [ 32 | {file = "anyio-4.4.0-py3-none-any.whl", hash = "sha256:c1b2d8f46a8a812513012e1107cb0e68c17159a7a594208005a57dc776e1bdc7"}, 33 | {file = "anyio-4.4.0.tar.gz", hash = "sha256:5aadc6a1bbb7cdb0bede386cac5e2940f5e2ff3aa20277e991cf028e0585ce94"}, 34 | ] 35 | 36 | [package.dependencies] 37 | exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} 38 | idna = ">=2.8" 39 | sniffio = ">=1.1" 40 | typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} 41 | 42 | [package.extras] 43 | doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] 44 | test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] 45 | trio = ["trio (>=0.23)"] 46 | 47 | [[package]] 48 | name = "black" 49 | version = "24.4.2" 50 | description = "The uncompromising code formatter." 51 | optional = false 52 | python-versions = ">=3.8" 53 | files = [ 54 | {file = "black-24.4.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd1b5a14e417189db4c7b64a6540f31730713d173f0b63e55fabd52d61d8fdce"}, 55 | {file = "black-24.4.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e537d281831ad0e71007dcdcbe50a71470b978c453fa41ce77186bbe0ed6021"}, 56 | {file = "black-24.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eaea3008c281f1038edb473c1aa8ed8143a5535ff18f978a318f10302b254063"}, 57 | {file = "black-24.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:7768a0dbf16a39aa5e9a3ded568bb545c8c2727396d063bbaf847df05b08cd96"}, 58 | {file = "black-24.4.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:257d724c2c9b1660f353b36c802ccece186a30accc7742c176d29c146df6e474"}, 59 | {file = "black-24.4.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bdde6f877a18f24844e381d45e9947a49e97933573ac9d4345399be37621e26c"}, 60 | {file = "black-24.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e151054aa00bad1f4e1f04919542885f89f5f7d086b8a59e5000e6c616896ffb"}, 61 | {file = "black-24.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:7e122b1c4fb252fd85df3ca93578732b4749d9be076593076ef4d07a0233c3e1"}, 62 | {file = "black-24.4.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:accf49e151c8ed2c0cdc528691838afd217c50412534e876a19270fea1e28e2d"}, 63 | {file = "black-24.4.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:88c57dc656038f1ab9f92b3eb5335ee9b021412feaa46330d5eba4e51fe49b04"}, 64 | {file = "black-24.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:be8bef99eb46d5021bf053114442914baeb3649a89dc5f3a555c88737e5e98fc"}, 65 | {file = "black-24.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:415e686e87dbbe6f4cd5ef0fbf764af7b89f9057b97c908742b6008cc554b9c0"}, 66 | {file = "black-24.4.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:bf10f7310db693bb62692609b397e8d67257c55f949abde4c67f9cc574492cc7"}, 67 | {file = "black-24.4.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:98e123f1d5cfd42f886624d84464f7756f60ff6eab89ae845210631714f6db94"}, 68 | {file = "black-24.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:48a85f2cb5e6799a9ef05347b476cce6c182d6c71ee36925a6c194d074336ef8"}, 69 | {file = "black-24.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:b1530ae42e9d6d5b670a34db49a94115a64596bc77710b1d05e9801e62ca0a7c"}, 70 | {file = "black-24.4.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:37aae07b029fa0174d39daf02748b379399b909652a806e5708199bd93899da1"}, 71 | {file = "black-24.4.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:da33a1a5e49c4122ccdfd56cd021ff1ebc4a1ec4e2d01594fef9b6f267a9e741"}, 72 | {file = "black-24.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ef703f83fc32e131e9bcc0a5094cfe85599e7109f896fe8bc96cc402f3eb4b6e"}, 73 | {file = "black-24.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:b9176b9832e84308818a99a561e90aa479e73c523b3f77afd07913380ae2eab7"}, 74 | {file = "black-24.4.2-py3-none-any.whl", hash = "sha256:d36ed1124bb81b32f8614555b34cc4259c3fbc7eec17870e8ff8ded335b58d8c"}, 75 | {file = "black-24.4.2.tar.gz", hash = "sha256:c872b53057f000085da66a19c55d68f6f8ddcac2642392ad3a355878406fbd4d"}, 76 | ] 77 | 78 | [package.dependencies] 79 | click = ">=8.0.0" 80 | mypy-extensions = ">=0.4.3" 81 | packaging = ">=22.0" 82 | pathspec = ">=0.9.0" 83 | platformdirs = ">=2" 84 | tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} 85 | typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} 86 | 87 | [package.extras] 88 | colorama = ["colorama (>=0.4.3)"] 89 | d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] 90 | jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] 91 | uvloop = ["uvloop (>=0.15.2)"] 92 | 93 | [[package]] 94 | name = "certifi" 95 | version = "2024.7.4" 96 | description = "Python package for providing Mozilla's CA Bundle." 97 | optional = false 98 | python-versions = ">=3.6" 99 | files = [ 100 | {file = "certifi-2024.7.4-py3-none-any.whl", hash = "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90"}, 101 | {file = "certifi-2024.7.4.tar.gz", hash = "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b"}, 102 | ] 103 | 104 | [[package]] 105 | name = "click" 106 | version = "8.1.7" 107 | description = "Composable command line interface toolkit" 108 | optional = false 109 | python-versions = ">=3.7" 110 | files = [ 111 | {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, 112 | {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, 113 | ] 114 | 115 | [package.dependencies] 116 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 117 | 118 | [[package]] 119 | name = "colorama" 120 | version = "0.4.6" 121 | description = "Cross-platform colored terminal text." 122 | optional = false 123 | python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" 124 | files = [ 125 | {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, 126 | {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, 127 | ] 128 | 129 | [[package]] 130 | name = "distro" 131 | version = "1.9.0" 132 | description = "Distro - an OS platform information API" 133 | optional = false 134 | python-versions = ">=3.6" 135 | files = [ 136 | {file = "distro-1.9.0-py3-none-any.whl", hash = "sha256:7bffd925d65168f85027d8da9af6bddab658135b840670a223589bc0c8ef02b2"}, 137 | {file = "distro-1.9.0.tar.gz", hash = "sha256:2fa77c6fd8940f116ee1d6b94a2f90b13b5ea8d019b98bc8bafdcabcdd9bdbed"}, 138 | ] 139 | 140 | [[package]] 141 | name = "exceptiongroup" 142 | version = "1.2.2" 143 | description = "Backport of PEP 654 (exception groups)" 144 | optional = false 145 | python-versions = ">=3.7" 146 | files = [ 147 | {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, 148 | {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, 149 | ] 150 | 151 | [package.extras] 152 | test = ["pytest (>=6)"] 153 | 154 | [[package]] 155 | name = "h11" 156 | version = "0.14.0" 157 | description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" 158 | optional = false 159 | python-versions = ">=3.7" 160 | files = [ 161 | {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, 162 | {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, 163 | ] 164 | 165 | [[package]] 166 | name = "httpcore" 167 | version = "1.0.5" 168 | description = "A minimal low-level HTTP client." 169 | optional = false 170 | python-versions = ">=3.8" 171 | files = [ 172 | {file = "httpcore-1.0.5-py3-none-any.whl", hash = "sha256:421f18bac248b25d310f3cacd198d55b8e6125c107797b609ff9b7a6ba7991b5"}, 173 | {file = "httpcore-1.0.5.tar.gz", hash = "sha256:34a38e2f9291467ee3b44e89dd52615370e152954ba21721378a87b2960f7a61"}, 174 | ] 175 | 176 | [package.dependencies] 177 | certifi = "*" 178 | h11 = ">=0.13,<0.15" 179 | 180 | [package.extras] 181 | asyncio = ["anyio (>=4.0,<5.0)"] 182 | http2 = ["h2 (>=3,<5)"] 183 | socks = ["socksio (==1.*)"] 184 | trio = ["trio (>=0.22.0,<0.26.0)"] 185 | 186 | [[package]] 187 | name = "httpx" 188 | version = "0.27.0" 189 | description = "The next generation HTTP client." 190 | optional = false 191 | python-versions = ">=3.8" 192 | files = [ 193 | {file = "httpx-0.27.0-py3-none-any.whl", hash = "sha256:71d5465162c13681bff01ad59b2cc68dd838ea1f10e51574bac27103f00c91a5"}, 194 | {file = "httpx-0.27.0.tar.gz", hash = "sha256:a0cb88a46f32dc874e04ee956e4c2764aba2aa228f650b06788ba6bda2962ab5"}, 195 | ] 196 | 197 | [package.dependencies] 198 | anyio = "*" 199 | certifi = "*" 200 | httpcore = "==1.*" 201 | idna = "*" 202 | sniffio = "*" 203 | 204 | [package.extras] 205 | brotli = ["brotli", "brotlicffi"] 206 | cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] 207 | http2 = ["h2 (>=3,<5)"] 208 | socks = ["socksio (==1.*)"] 209 | 210 | [[package]] 211 | name = "idna" 212 | version = "3.7" 213 | description = "Internationalized Domain Names in Applications (IDNA)" 214 | optional = false 215 | python-versions = ">=3.5" 216 | files = [ 217 | {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, 218 | {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, 219 | ] 220 | 221 | [[package]] 222 | name = "iniconfig" 223 | version = "2.0.0" 224 | description = "brain-dead simple config-ini parsing" 225 | optional = false 226 | python-versions = ">=3.7" 227 | files = [ 228 | {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, 229 | {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, 230 | ] 231 | 232 | [[package]] 233 | name = "isort" 234 | version = "5.13.2" 235 | description = "A Python utility / library to sort Python imports." 236 | optional = false 237 | python-versions = ">=3.8.0" 238 | files = [ 239 | {file = "isort-5.13.2-py3-none-any.whl", hash = "sha256:8ca5e72a8d85860d5a3fa69b8745237f2939afe12dbf656afbcb47fe72d947a6"}, 240 | {file = "isort-5.13.2.tar.gz", hash = "sha256:48fdfcb9face5d58a4f6dde2e72a1fb8dcaf8ab26f95ab49fab84c2ddefb0109"}, 241 | ] 242 | 243 | [package.extras] 244 | colors = ["colorama (>=0.4.6)"] 245 | 246 | [[package]] 247 | name = "loguru" 248 | version = "0.7.2" 249 | description = "Python logging made (stupidly) simple" 250 | optional = false 251 | python-versions = ">=3.5" 252 | files = [ 253 | {file = "loguru-0.7.2-py3-none-any.whl", hash = "sha256:003d71e3d3ed35f0f8984898359d65b79e5b21943f78af86aa5491210429b8eb"}, 254 | {file = "loguru-0.7.2.tar.gz", hash = "sha256:e671a53522515f34fd406340ee968cb9ecafbc4b36c679da03c18fd8d0bd51ac"}, 255 | ] 256 | 257 | [package.dependencies] 258 | colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} 259 | win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} 260 | 261 | [package.extras] 262 | dev = ["Sphinx (==7.2.5)", "colorama (==0.4.5)", "colorama (==0.4.6)", "exceptiongroup (==1.1.3)", "freezegun (==1.1.0)", "freezegun (==1.2.2)", "mypy (==v0.910)", "mypy (==v0.971)", "mypy (==v1.4.1)", "mypy (==v1.5.1)", "pre-commit (==3.4.0)", "pytest (==6.1.2)", "pytest (==7.4.0)", "pytest-cov (==2.12.1)", "pytest-cov (==4.1.0)", "pytest-mypy-plugins (==1.9.3)", "pytest-mypy-plugins (==3.0.0)", "sphinx-autobuild (==2021.3.14)", "sphinx-rtd-theme (==1.3.0)", "tox (==3.27.1)", "tox (==4.11.0)"] 263 | 264 | [[package]] 265 | name = "markdown-it-py" 266 | version = "3.0.0" 267 | description = "Python port of markdown-it. Markdown parsing, done right!" 268 | optional = false 269 | python-versions = ">=3.8" 270 | files = [ 271 | {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, 272 | {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, 273 | ] 274 | 275 | [package.dependencies] 276 | mdurl = ">=0.1,<1.0" 277 | 278 | [package.extras] 279 | benchmarking = ["psutil", "pytest", "pytest-benchmark"] 280 | code-style = ["pre-commit (>=3.0,<4.0)"] 281 | compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] 282 | linkify = ["linkify-it-py (>=1,<3)"] 283 | plugins = ["mdit-py-plugins"] 284 | profiling = ["gprof2dot"] 285 | rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] 286 | testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] 287 | 288 | [[package]] 289 | name = "mdurl" 290 | version = "0.1.2" 291 | description = "Markdown URL utilities" 292 | optional = false 293 | python-versions = ">=3.7" 294 | files = [ 295 | {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, 296 | {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, 297 | ] 298 | 299 | [[package]] 300 | name = "mypy-extensions" 301 | version = "1.0.0" 302 | description = "Type system extensions for programs checked with the mypy type checker." 303 | optional = false 304 | python-versions = ">=3.5" 305 | files = [ 306 | {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, 307 | {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, 308 | ] 309 | 310 | [[package]] 311 | name = "openai" 312 | version = "1.37.0" 313 | description = "The official Python library for the openai API" 314 | optional = false 315 | python-versions = ">=3.7.1" 316 | files = [ 317 | {file = "openai-1.37.0-py3-none-any.whl", hash = "sha256:a903245c0ecf622f2830024acdaa78683c70abb8e9d37a497b851670864c9f73"}, 318 | {file = "openai-1.37.0.tar.gz", hash = "sha256:dc8197fc40ab9d431777b6620d962cc49f4544ffc3011f03ce0a805e6eb54adb"}, 319 | ] 320 | 321 | [package.dependencies] 322 | anyio = ">=3.5.0,<5" 323 | distro = ">=1.7.0,<2" 324 | httpx = ">=0.23.0,<1" 325 | pydantic = ">=1.9.0,<3" 326 | sniffio = "*" 327 | tqdm = ">4" 328 | typing-extensions = ">=4.7,<5" 329 | 330 | [package.extras] 331 | datalib = ["numpy (>=1)", "pandas (>=1.2.3)", "pandas-stubs (>=1.1.0.11)"] 332 | 333 | [[package]] 334 | name = "packaging" 335 | version = "24.1" 336 | description = "Core utilities for Python packages" 337 | optional = false 338 | python-versions = ">=3.8" 339 | files = [ 340 | {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, 341 | {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, 342 | ] 343 | 344 | [[package]] 345 | name = "pathspec" 346 | version = "0.12.1" 347 | description = "Utility library for gitignore style pattern matching of file paths." 348 | optional = false 349 | python-versions = ">=3.8" 350 | files = [ 351 | {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, 352 | {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, 353 | ] 354 | 355 | [[package]] 356 | name = "platformdirs" 357 | version = "4.2.2" 358 | description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." 359 | optional = false 360 | python-versions = ">=3.8" 361 | files = [ 362 | {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, 363 | {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, 364 | ] 365 | 366 | [package.extras] 367 | docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] 368 | test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] 369 | type = ["mypy (>=1.8)"] 370 | 371 | [[package]] 372 | name = "pluggy" 373 | version = "1.5.0" 374 | description = "plugin and hook calling mechanisms for python" 375 | optional = false 376 | python-versions = ">=3.8" 377 | files = [ 378 | {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, 379 | {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, 380 | ] 381 | 382 | [package.extras] 383 | dev = ["pre-commit", "tox"] 384 | testing = ["pytest", "pytest-benchmark"] 385 | 386 | [[package]] 387 | name = "pydantic" 388 | version = "2.8.2" 389 | description = "Data validation using Python type hints" 390 | optional = false 391 | python-versions = ">=3.8" 392 | files = [ 393 | {file = "pydantic-2.8.2-py3-none-any.whl", hash = "sha256:73ee9fddd406dc318b885c7a2eab8a6472b68b8fb5ba8150949fc3db939f23c8"}, 394 | {file = "pydantic-2.8.2.tar.gz", hash = "sha256:6f62c13d067b0755ad1c21a34bdd06c0c12625a22b0fc09c6b149816604f7c2a"}, 395 | ] 396 | 397 | [package.dependencies] 398 | annotated-types = ">=0.4.0" 399 | pydantic-core = "2.20.1" 400 | typing-extensions = [ 401 | {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, 402 | {version = ">=4.6.1", markers = "python_version < \"3.13\""}, 403 | ] 404 | 405 | [package.extras] 406 | email = ["email-validator (>=2.0.0)"] 407 | 408 | [[package]] 409 | name = "pydantic-core" 410 | version = "2.20.1" 411 | description = "Core functionality for Pydantic validation and serialization" 412 | optional = false 413 | python-versions = ">=3.8" 414 | files = [ 415 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3acae97ffd19bf091c72df4d726d552c473f3576409b2a7ca36b2f535ffff4a3"}, 416 | {file = "pydantic_core-2.20.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:41f4c96227a67a013e7de5ff8f20fb496ce573893b7f4f2707d065907bffdbd6"}, 417 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f239eb799a2081495ea659d8d4a43a8f42cd1fe9ff2e7e436295c38a10c286a"}, 418 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:53e431da3fc53360db73eedf6f7124d1076e1b4ee4276b36fb25514544ceb4a3"}, 419 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1f62b2413c3a0e846c3b838b2ecd6c7a19ec6793b2a522745b0869e37ab5bc1"}, 420 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5d41e6daee2813ecceea8eda38062d69e280b39df793f5a942fa515b8ed67953"}, 421 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d482efec8b7dc6bfaedc0f166b2ce349df0011f5d2f1f25537ced4cfc34fd98"}, 422 | {file = "pydantic_core-2.20.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e93e1a4b4b33daed65d781a57a522ff153dcf748dee70b40c7258c5861e1768a"}, 423 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e7c4ea22b6739b162c9ecaaa41d718dfad48a244909fe7ef4b54c0b530effc5a"}, 424 | {file = "pydantic_core-2.20.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4f2790949cf385d985a31984907fecb3896999329103df4e4983a4a41e13e840"}, 425 | {file = "pydantic_core-2.20.1-cp310-none-win32.whl", hash = "sha256:5e999ba8dd90e93d57410c5e67ebb67ffcaadcea0ad973240fdfd3a135506250"}, 426 | {file = "pydantic_core-2.20.1-cp310-none-win_amd64.whl", hash = "sha256:512ecfbefef6dac7bc5eaaf46177b2de58cdf7acac8793fe033b24ece0b9566c"}, 427 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:d2a8fa9d6d6f891f3deec72f5cc668e6f66b188ab14bb1ab52422fe8e644f312"}, 428 | {file = "pydantic_core-2.20.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:175873691124f3d0da55aeea1d90660a6ea7a3cfea137c38afa0a5ffabe37b88"}, 429 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37eee5b638f0e0dcd18d21f59b679686bbd18917b87db0193ae36f9c23c355fc"}, 430 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25e9185e2d06c16ee438ed39bf62935ec436474a6ac4f9358524220f1b236e43"}, 431 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:150906b40ff188a3260cbee25380e7494ee85048584998c1e66df0c7a11c17a6"}, 432 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8ad4aeb3e9a97286573c03df758fc7627aecdd02f1da04516a86dc159bf70121"}, 433 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d3f3ed29cd9f978c604708511a1f9c2fdcb6c38b9aae36a51905b8811ee5cbf1"}, 434 | {file = "pydantic_core-2.20.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b0dae11d8f5ded51699c74d9548dcc5938e0804cc8298ec0aa0da95c21fff57b"}, 435 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:faa6b09ee09433b87992fb5a2859efd1c264ddc37280d2dd5db502126d0e7f27"}, 436 | {file = "pydantic_core-2.20.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:9dc1b507c12eb0481d071f3c1808f0529ad41dc415d0ca11f7ebfc666e66a18b"}, 437 | {file = "pydantic_core-2.20.1-cp311-none-win32.whl", hash = "sha256:fa2fddcb7107e0d1808086ca306dcade7df60a13a6c347a7acf1ec139aa6789a"}, 438 | {file = "pydantic_core-2.20.1-cp311-none-win_amd64.whl", hash = "sha256:40a783fb7ee353c50bd3853e626f15677ea527ae556429453685ae32280c19c2"}, 439 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:595ba5be69b35777474fa07f80fc260ea71255656191adb22a8c53aba4479231"}, 440 | {file = "pydantic_core-2.20.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a4f55095ad087474999ee28d3398bae183a66be4823f753cd7d67dd0153427c9"}, 441 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f9aa05d09ecf4c75157197f27cdc9cfaeb7c5f15021c6373932bf3e124af029f"}, 442 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e97fdf088d4b31ff4ba35db26d9cc472ac7ef4a2ff2badeabf8d727b3377fc52"}, 443 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc633a9fe1eb87e250b5c57d389cf28998e4292336926b0b6cdaee353f89a237"}, 444 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d573faf8eb7e6b1cbbcb4f5b247c60ca8be39fe2c674495df0eb4318303137fe"}, 445 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:26dc97754b57d2fd00ac2b24dfa341abffc380b823211994c4efac7f13b9e90e"}, 446 | {file = "pydantic_core-2.20.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:33499e85e739a4b60c9dac710c20a08dc73cb3240c9a0e22325e671b27b70d24"}, 447 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bebb4d6715c814597f85297c332297c6ce81e29436125ca59d1159b07f423eb1"}, 448 | {file = "pydantic_core-2.20.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:516d9227919612425c8ef1c9b869bbbee249bc91912c8aaffb66116c0b447ebd"}, 449 | {file = "pydantic_core-2.20.1-cp312-none-win32.whl", hash = "sha256:469f29f9093c9d834432034d33f5fe45699e664f12a13bf38c04967ce233d688"}, 450 | {file = "pydantic_core-2.20.1-cp312-none-win_amd64.whl", hash = "sha256:035ede2e16da7281041f0e626459bcae33ed998cca6a0a007a5ebb73414ac72d"}, 451 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:0827505a5c87e8aa285dc31e9ec7f4a17c81a813d45f70b1d9164e03a813a686"}, 452 | {file = "pydantic_core-2.20.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:19c0fa39fa154e7e0b7f82f88ef85faa2a4c23cc65aae2f5aea625e3c13c735a"}, 453 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa223cd1e36b642092c326d694d8bf59b71ddddc94cdb752bbbb1c5c91d833b"}, 454 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c336a6d235522a62fef872c6295a42ecb0c4e1d0f1a3e500fe949415761b8a19"}, 455 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7eb6a0587eded33aeefea9f916899d42b1799b7b14b8f8ff2753c0ac1741edac"}, 456 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:70c8daf4faca8da5a6d655f9af86faf6ec2e1768f4b8b9d0226c02f3d6209703"}, 457 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9fa4c9bf273ca41f940bceb86922a7667cd5bf90e95dbb157cbb8441008482c"}, 458 | {file = "pydantic_core-2.20.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:11b71d67b4725e7e2a9f6e9c0ac1239bbc0c48cce3dc59f98635efc57d6dac83"}, 459 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:270755f15174fb983890c49881e93f8f1b80f0b5e3a3cc1394a255706cabd203"}, 460 | {file = "pydantic_core-2.20.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c81131869240e3e568916ef4c307f8b99583efaa60a8112ef27a366eefba8ef0"}, 461 | {file = "pydantic_core-2.20.1-cp313-none-win32.whl", hash = "sha256:b91ced227c41aa29c672814f50dbb05ec93536abf8f43cd14ec9521ea09afe4e"}, 462 | {file = "pydantic_core-2.20.1-cp313-none-win_amd64.whl", hash = "sha256:65db0f2eefcaad1a3950f498aabb4875c8890438bc80b19362cf633b87a8ab20"}, 463 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:4745f4ac52cc6686390c40eaa01d48b18997cb130833154801a442323cc78f91"}, 464 | {file = "pydantic_core-2.20.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:a8ad4c766d3f33ba8fd692f9aa297c9058970530a32c728a2c4bfd2616d3358b"}, 465 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:41e81317dd6a0127cabce83c0c9c3fbecceae981c8391e6f1dec88a77c8a569a"}, 466 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:04024d270cf63f586ad41fff13fde4311c4fc13ea74676962c876d9577bcc78f"}, 467 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eaad4ff2de1c3823fddf82f41121bdf453d922e9a238642b1dedb33c4e4f98ad"}, 468 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26ab812fa0c845df815e506be30337e2df27e88399b985d0bb4e3ecfe72df31c"}, 469 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3c5ebac750d9d5f2706654c638c041635c385596caf68f81342011ddfa1e5598"}, 470 | {file = "pydantic_core-2.20.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2aafc5a503855ea5885559eae883978c9b6d8c8993d67766ee73d82e841300dd"}, 471 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:4868f6bd7c9d98904b748a2653031fc9c2f85b6237009d475b1008bfaeb0a5aa"}, 472 | {file = "pydantic_core-2.20.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa2f457b4af386254372dfa78a2eda2563680d982422641a85f271c859df1987"}, 473 | {file = "pydantic_core-2.20.1-cp38-none-win32.whl", hash = "sha256:225b67a1f6d602de0ce7f6c1c3ae89a4aa25d3de9be857999e9124f15dab486a"}, 474 | {file = "pydantic_core-2.20.1-cp38-none-win_amd64.whl", hash = "sha256:6b507132dcfc0dea440cce23ee2182c0ce7aba7054576efc65634f080dbe9434"}, 475 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b03f7941783b4c4a26051846dea594628b38f6940a2fdc0df00b221aed39314c"}, 476 | {file = "pydantic_core-2.20.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1eedfeb6089ed3fad42e81a67755846ad4dcc14d73698c120a82e4ccf0f1f9f6"}, 477 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:635fee4e041ab9c479e31edda27fcf966ea9614fff1317e280d99eb3e5ab6fe2"}, 478 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:77bf3ac639c1ff567ae3b47f8d4cc3dc20f9966a2a6dd2311dcc055d3d04fb8a"}, 479 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ed1b0132f24beeec5a78b67d9388656d03e6a7c837394f99257e2d55b461611"}, 480 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6514f963b023aeee506678a1cf821fe31159b925c4b76fe2afa94cc70b3222b"}, 481 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10d4204d8ca33146e761c79f83cc861df20e7ae9f6487ca290a97702daf56006"}, 482 | {file = "pydantic_core-2.20.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d036c7187b9422ae5b262badb87a20a49eb6c5238b2004e96d4da1231badef1"}, 483 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:9ebfef07dbe1d93efb94b4700f2d278494e9162565a54f124c404a5656d7ff09"}, 484 | {file = "pydantic_core-2.20.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:6b9d9bb600328a1ce523ab4f454859e9d439150abb0906c5a1983c146580ebab"}, 485 | {file = "pydantic_core-2.20.1-cp39-none-win32.whl", hash = "sha256:784c1214cb6dd1e3b15dd8b91b9a53852aed16671cc3fbe4786f4f1db07089e2"}, 486 | {file = "pydantic_core-2.20.1-cp39-none-win_amd64.whl", hash = "sha256:d2fe69c5434391727efa54b47a1e7986bb0186e72a41b203df8f5b0a19a4f669"}, 487 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:a45f84b09ac9c3d35dfcf6a27fd0634d30d183205230a0ebe8373a0e8cfa0906"}, 488 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:d02a72df14dfdbaf228424573a07af10637bd490f0901cee872c4f434a735b94"}, 489 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d2b27e6af28f07e2f195552b37d7d66b150adbaa39a6d327766ffd695799780f"}, 490 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:084659fac3c83fd674596612aeff6041a18402f1e1bc19ca39e417d554468482"}, 491 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:242b8feb3c493ab78be289c034a1f659e8826e2233786e36f2893a950a719bb6"}, 492 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:38cf1c40a921d05c5edc61a785c0ddb4bed67827069f535d794ce6bcded919fc"}, 493 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:e0bbdd76ce9aa5d4209d65f2b27fc6e5ef1312ae6c5333c26db3f5ade53a1e99"}, 494 | {file = "pydantic_core-2.20.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:254ec27fdb5b1ee60684f91683be95e5133c994cc54e86a0b0963afa25c8f8a6"}, 495 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:407653af5617f0757261ae249d3fba09504d7a71ab36ac057c938572d1bc9331"}, 496 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:c693e916709c2465b02ca0ad7b387c4f8423d1db7b4649c551f27a529181c5ad"}, 497 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5b5ff4911aea936a47d9376fd3ab17e970cc543d1b68921886e7f64bd28308d1"}, 498 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f55a886d74f1808763976ac4efd29b7ed15c69f4d838bbd74d9d09cf6fa86"}, 499 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:964faa8a861d2664f0c7ab0c181af0bea66098b1919439815ca8803ef136fc4e"}, 500 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:4dd484681c15e6b9a977c785a345d3e378d72678fd5f1f3c0509608da24f2ac0"}, 501 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f6d6cff3538391e8486a431569b77921adfcdef14eb18fbf19b7c0a5294d4e6a"}, 502 | {file = "pydantic_core-2.20.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:a6d511cc297ff0883bc3708b465ff82d7560193169a8b93260f74ecb0a5e08a7"}, 503 | {file = "pydantic_core-2.20.1.tar.gz", hash = "sha256:26ca695eeee5f9f1aeeb211ffc12f10bcb6f71e2989988fda61dabd65db878d4"}, 504 | ] 505 | 506 | [package.dependencies] 507 | typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" 508 | 509 | [[package]] 510 | name = "pydantic-settings" 511 | version = "2.3.4" 512 | description = "Settings management using Pydantic" 513 | optional = false 514 | python-versions = ">=3.8" 515 | files = [ 516 | {file = "pydantic_settings-2.3.4-py3-none-any.whl", hash = "sha256:11ad8bacb68a045f00e4f862c7a718c8a9ec766aa8fd4c32e39a0594b207b53a"}, 517 | {file = "pydantic_settings-2.3.4.tar.gz", hash = "sha256:c5802e3d62b78e82522319bbc9b8f8ffb28ad1c988a99311d04f2a6051fca0a7"}, 518 | ] 519 | 520 | [package.dependencies] 521 | pydantic = ">=2.7.0" 522 | python-dotenv = ">=0.21.0" 523 | 524 | [package.extras] 525 | toml = ["tomli (>=2.0.1)"] 526 | yaml = ["pyyaml (>=6.0.1)"] 527 | 528 | [[package]] 529 | name = "pygments" 530 | version = "2.18.0" 531 | description = "Pygments is a syntax highlighting package written in Python." 532 | optional = false 533 | python-versions = ">=3.8" 534 | files = [ 535 | {file = "pygments-2.18.0-py3-none-any.whl", hash = "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a"}, 536 | {file = "pygments-2.18.0.tar.gz", hash = "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199"}, 537 | ] 538 | 539 | [package.extras] 540 | windows-terminal = ["colorama (>=0.4.6)"] 541 | 542 | [[package]] 543 | name = "pytest" 544 | version = "8.3.2" 545 | description = "pytest: simple powerful testing with Python" 546 | optional = false 547 | python-versions = ">=3.8" 548 | files = [ 549 | {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, 550 | {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, 551 | ] 552 | 553 | [package.dependencies] 554 | colorama = {version = "*", markers = "sys_platform == \"win32\""} 555 | exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} 556 | iniconfig = "*" 557 | packaging = "*" 558 | pluggy = ">=1.5,<2" 559 | tomli = {version = ">=1", markers = "python_version < \"3.11\""} 560 | 561 | [package.extras] 562 | dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] 563 | 564 | [[package]] 565 | name = "pytest-asyncio" 566 | version = "0.23.8" 567 | description = "Pytest support for asyncio" 568 | optional = false 569 | python-versions = ">=3.8" 570 | files = [ 571 | {file = "pytest_asyncio-0.23.8-py3-none-any.whl", hash = "sha256:50265d892689a5faefb84df80819d1ecef566eb3549cf915dfb33569359d1ce2"}, 572 | {file = "pytest_asyncio-0.23.8.tar.gz", hash = "sha256:759b10b33a6dc61cce40a8bd5205e302978bbbcc00e279a8b61d9a6a3c82e4d3"}, 573 | ] 574 | 575 | [package.dependencies] 576 | pytest = ">=7.0.0,<9" 577 | 578 | [package.extras] 579 | docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] 580 | testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] 581 | 582 | [[package]] 583 | name = "pytest-mock" 584 | version = "3.14.0" 585 | description = "Thin-wrapper around the mock package for easier use with pytest" 586 | optional = false 587 | python-versions = ">=3.8" 588 | files = [ 589 | {file = "pytest-mock-3.14.0.tar.gz", hash = "sha256:2719255a1efeceadbc056d6bf3df3d1c5015530fb40cf347c0f9afac88410bd0"}, 590 | {file = "pytest_mock-3.14.0-py3-none-any.whl", hash = "sha256:0b72c38033392a5f4621342fe11e9219ac11ec9d375f8e2a0c164539e0d70f6f"}, 591 | ] 592 | 593 | [package.dependencies] 594 | pytest = ">=6.2.5" 595 | 596 | [package.extras] 597 | dev = ["pre-commit", "pytest-asyncio", "tox"] 598 | 599 | [[package]] 600 | name = "python-dotenv" 601 | version = "1.0.1" 602 | description = "Read key-value pairs from a .env file and set them as environment variables" 603 | optional = false 604 | python-versions = ">=3.8" 605 | files = [ 606 | {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, 607 | {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, 608 | ] 609 | 610 | [package.extras] 611 | cli = ["click (>=5.0)"] 612 | 613 | [[package]] 614 | name = "rich" 615 | version = "13.7.1" 616 | description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" 617 | optional = false 618 | python-versions = ">=3.7.0" 619 | files = [ 620 | {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, 621 | {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, 622 | ] 623 | 624 | [package.dependencies] 625 | markdown-it-py = ">=2.2.0" 626 | pygments = ">=2.13.0,<3.0.0" 627 | 628 | [package.extras] 629 | jupyter = ["ipywidgets (>=7.5.1,<9)"] 630 | 631 | [[package]] 632 | name = "ruff" 633 | version = "0.5.5" 634 | description = "An extremely fast Python linter and code formatter, written in Rust." 635 | optional = false 636 | python-versions = ">=3.7" 637 | files = [ 638 | {file = "ruff-0.5.5-py3-none-linux_armv6l.whl", hash = "sha256:605d589ec35d1da9213a9d4d7e7a9c761d90bba78fc8790d1c5e65026c1b9eaf"}, 639 | {file = "ruff-0.5.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:00817603822a3e42b80f7c3298c8269e09f889ee94640cd1fc7f9329788d7bf8"}, 640 | {file = "ruff-0.5.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:187a60f555e9f865a2ff2c6984b9afeffa7158ba6e1eab56cb830404c942b0f3"}, 641 | {file = "ruff-0.5.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe26fc46fa8c6e0ae3f47ddccfbb136253c831c3289bba044befe68f467bfb16"}, 642 | {file = "ruff-0.5.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:4ad25dd9c5faac95c8e9efb13e15803cd8bbf7f4600645a60ffe17c73f60779b"}, 643 | {file = "ruff-0.5.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f70737c157d7edf749bcb952d13854e8f745cec695a01bdc6e29c29c288fc36e"}, 644 | {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:cfd7de17cef6ab559e9f5ab859f0d3296393bc78f69030967ca4d87a541b97a0"}, 645 | {file = "ruff-0.5.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a09b43e02f76ac0145f86a08e045e2ea452066f7ba064fd6b0cdccb486f7c3e7"}, 646 | {file = "ruff-0.5.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0b856cb19c60cd40198be5d8d4b556228e3dcd545b4f423d1ad812bfdca5884"}, 647 | {file = "ruff-0.5.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3687d002f911e8a5faf977e619a034d159a8373514a587249cc00f211c67a091"}, 648 | {file = "ruff-0.5.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ac9dc814e510436e30d0ba535f435a7f3dc97f895f844f5b3f347ec8c228a523"}, 649 | {file = "ruff-0.5.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:af9bdf6c389b5add40d89b201425b531e0a5cceb3cfdcc69f04d3d531c6be74f"}, 650 | {file = "ruff-0.5.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:d40a8533ed545390ef8315b8e25c4bb85739b90bd0f3fe1280a29ae364cc55d8"}, 651 | {file = "ruff-0.5.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:cab904683bf9e2ecbbe9ff235bfe056f0eba754d0168ad5407832928d579e7ab"}, 652 | {file = "ruff-0.5.5-py3-none-win32.whl", hash = "sha256:696f18463b47a94575db635ebb4c178188645636f05e934fdf361b74edf1bb2d"}, 653 | {file = "ruff-0.5.5-py3-none-win_amd64.whl", hash = "sha256:50f36d77f52d4c9c2f1361ccbfbd09099a1b2ea5d2b2222c586ab08885cf3445"}, 654 | {file = "ruff-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3191317d967af701f1b73a31ed5788795936e423b7acce82a2b63e26eb3e89d6"}, 655 | {file = "ruff-0.5.5.tar.gz", hash = "sha256:cc5516bdb4858d972fbc31d246bdb390eab8df1a26e2353be2dbc0c2d7f5421a"}, 656 | ] 657 | 658 | [[package]] 659 | name = "shellingham" 660 | version = "1.5.4" 661 | description = "Tool to Detect Surrounding Shell" 662 | optional = false 663 | python-versions = ">=3.7" 664 | files = [ 665 | {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, 666 | {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, 667 | ] 668 | 669 | [[package]] 670 | name = "sniffio" 671 | version = "1.3.1" 672 | description = "Sniff out which async library your code is running under" 673 | optional = false 674 | python-versions = ">=3.7" 675 | files = [ 676 | {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, 677 | {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, 678 | ] 679 | 680 | [[package]] 681 | name = "tomli" 682 | version = "2.0.1" 683 | description = "A lil' TOML parser" 684 | optional = false 685 | python-versions = ">=3.7" 686 | files = [ 687 | {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, 688 | {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, 689 | ] 690 | 691 | [[package]] 692 | name = "tqdm" 693 | version = "4.66.4" 694 | description = "Fast, Extensible Progress Meter" 695 | optional = false 696 | python-versions = ">=3.7" 697 | files = [ 698 | {file = "tqdm-4.66.4-py3-none-any.whl", hash = "sha256:b75ca56b413b030bc3f00af51fd2c1a1a5eac6a0c1cca83cbb37a5c52abce644"}, 699 | {file = "tqdm-4.66.4.tar.gz", hash = "sha256:e4d936c9de8727928f3be6079590e97d9abfe8d39a590be678eb5919ffc186bb"}, 700 | ] 701 | 702 | [package.dependencies] 703 | colorama = {version = "*", markers = "platform_system == \"Windows\""} 704 | 705 | [package.extras] 706 | dev = ["pytest (>=6)", "pytest-cov", "pytest-timeout", "pytest-xdist"] 707 | notebook = ["ipywidgets (>=6)"] 708 | slack = ["slack-sdk"] 709 | telegram = ["requests"] 710 | 711 | [[package]] 712 | name = "typer" 713 | version = "0.12.3" 714 | description = "Typer, build great CLIs. Easy to code. Based on Python type hints." 715 | optional = false 716 | python-versions = ">=3.7" 717 | files = [ 718 | {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, 719 | {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, 720 | ] 721 | 722 | [package.dependencies] 723 | click = ">=8.0.0" 724 | rich = ">=10.11.0" 725 | shellingham = ">=1.3.0" 726 | typing-extensions = ">=3.7.4.3" 727 | 728 | [[package]] 729 | name = "typing-extensions" 730 | version = "4.12.2" 731 | description = "Backported and Experimental Type Hints for Python 3.8+" 732 | optional = false 733 | python-versions = ">=3.8" 734 | files = [ 735 | {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, 736 | {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, 737 | ] 738 | 739 | [[package]] 740 | name = "win32-setctime" 741 | version = "1.1.0" 742 | description = "A small Python utility to set file creation time on Windows" 743 | optional = false 744 | python-versions = ">=3.5" 745 | files = [ 746 | {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, 747 | {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, 748 | ] 749 | 750 | [package.extras] 751 | dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] 752 | 753 | [metadata] 754 | lock-version = "2.0" 755 | python-versions = "^3.9" 756 | content-hash = "b706f32a55a5097d1ea4139731c80d288635f3af8af2b97448e1fd7e728254a9" 757 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "batchwizard" 3 | version = "0.2.0" 4 | description = "BatchWizard: Manage OpenAI batch processing jobs with ease" 5 | authors = ["Carl Kugblenu"] 6 | license = "MIT" 7 | readme = "README.md" 8 | homepage = "https://github.com/cmakafui/batchwizard" 9 | repository = "https://github.com/cmakafui/batchwizard" 10 | keywords = ["openai", "batch", "cli", "async"] 11 | 12 | [tool.poetry.dependencies] 13 | python = "^3.9" 14 | typer = "^0.12.3" 15 | rich = "^13.7.1" 16 | openai = "^1.37.0" 17 | pydantic = "^2.8.2" 18 | aiofiles = "^24.1.0" 19 | loguru = "^0.7.2" 20 | pydantic-settings = "^2.3.4" 21 | python-dotenv = "^1.0.1" 22 | 23 | 24 | [tool.poetry.group.dev.dependencies] 25 | pytest = "^8.3.2" 26 | pytest-asyncio = "^0.23.8" 27 | isort = "^5.13.2" 28 | black = "^24.4.2" 29 | ruff = "^0.5.5" 30 | pytest-mock = "^3.14.0" 31 | 32 | [tool.poetry.scripts] 33 | batchwizard = "batchwizard.cli:app" 34 | 35 | [build-system] 36 | requires = ["poetry-core"] 37 | build-backend = "poetry.core.masonry.api" 38 | -------------------------------------------------------------------------------- /pytest.ini: -------------------------------------------------------------------------------- 1 | [pytest] 2 | pythonpath = src 3 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | # This file was autogenerated by uv via the following command: 2 | # uv pip compile pyproject.toml -o requirements.txt 3 | aiofiles==24.1.0 4 | annotated-types==0.7.0 5 | # via pydantic 6 | anyio==4.4.0 7 | # via 8 | # httpx 9 | # openai 10 | certifi==2024.7.4 11 | # via 12 | # httpcore 13 | # httpx 14 | click==8.1.7 15 | # via typer 16 | distro==1.9.0 17 | # via openai 18 | h11==0.14.0 19 | # via httpcore 20 | httpcore==1.0.5 21 | # via httpx 22 | httpx==0.27.0 23 | # via openai 24 | idna==3.7 25 | # via 26 | # anyio 27 | # httpx 28 | loguru==0.7.2 29 | markdown-it-py==3.0.0 30 | # via rich 31 | mdurl==0.1.2 32 | # via markdown-it-py 33 | openai==1.37.0 34 | pydantic==2.8.2 35 | # via 36 | # openai 37 | # pydantic-settings 38 | pydantic-core==2.20.1 39 | # via pydantic 40 | pydantic-settings==2.3.4 41 | pygments==2.18.0 42 | # via rich 43 | python-dotenv==1.0.1 44 | # via pydantic-settings 45 | rich==13.7.1 46 | # via typer 47 | shellingham==1.5.4 48 | # via typer 49 | sniffio==1.3.1 50 | # via 51 | # anyio 52 | # httpx 53 | # openai 54 | tqdm==4.66.4 55 | # via openai 56 | typer==0.12.3 57 | typing-extensions==4.12.2 58 | # via 59 | # openai 60 | # pydantic 61 | # pydantic-core 62 | # typer 63 | -------------------------------------------------------------------------------- /src/batchwizard/__init__.py: -------------------------------------------------------------------------------- 1 | # __init__.py 2 | from .cli import app 3 | from .models import BatchJob, BatchJobResult 4 | from .processor import BatchProcessor 5 | 6 | __all__ = ["BatchProcessor", "BatchJob", "BatchJobResult", "app"] 7 | -------------------------------------------------------------------------------- /src/batchwizard/cli.py: -------------------------------------------------------------------------------- 1 | # cli.py 2 | import asyncio 3 | from datetime import datetime 4 | from pathlib import Path 5 | from typing import Optional 6 | 7 | import typer 8 | from rich.console import Console 9 | from rich.table import Table 10 | 11 | from .config import BatchWizardSettings, config 12 | from .processor import BatchProcessor 13 | from .ui import BatchWizardUI 14 | from .utils import get_api_key, set_api_key, setup_logger 15 | 16 | app = typer.Typer(help="BatchWizard: Manage OpenAI batch processing jobs with ease") 17 | console = Console() 18 | logger = setup_logger(console) 19 | 20 | 21 | @app.command() 22 | def process( 23 | input_paths: list[Path] = typer.Argument( 24 | ..., help="Paths to input files or directories for processing" 25 | ), 26 | output_directory: Optional[Path] = typer.Option( 27 | None, help="Directory to store output files" 28 | ), 29 | max_concurrent_jobs: int = typer.Option( 30 | 5, help="Maximum number of concurrent jobs" 31 | ), 32 | check_interval: int = typer.Option( 33 | 5, help="Initial interval (in seconds) between job status checks" 34 | ), 35 | ): 36 | """Process batch jobs from input files or directories.""" 37 | if not output_directory: 38 | output_directory = Path.cwd() / "results" 39 | output_directory.mkdir(parents=True, exist_ok=True) 40 | 41 | api_key = get_api_key() 42 | if not api_key: 43 | logger.error( 44 | "API key not set. Please set it using 'openaibatch configure --set-key YOUR_API_KEY'" 45 | ) 46 | raise typer.Exit(code=1) 47 | 48 | config.settings.max_concurrent_jobs = max_concurrent_jobs 49 | config.settings.check_interval = check_interval 50 | config.save() 51 | 52 | processor = BatchProcessor() 53 | ui = BatchWizardUI(Console()) 54 | 55 | async def run_and_close(): 56 | try: 57 | await ui.run_processing(processor, input_paths, output_directory) 58 | finally: 59 | await processor.close() 60 | 61 | asyncio.run(run_and_close()) 62 | 63 | 64 | 65 | @app.command() 66 | def configure( 67 | set_key: Optional[str] = typer.Option( 68 | None, "--set-key", help="Set the OpenAI API key" 69 | ), 70 | show: bool = typer.Option(False, "--show", help="Show the current configuration"), 71 | reset: bool = typer.Option( 72 | False, "--reset", help="Reset the configuration to default values" 73 | ), 74 | ): 75 | """Manage BatchWizard configuration.""" 76 | if set_key: 77 | set_api_key(set_key) 78 | console.print("[green]API key set successfully.[/green]") 79 | elif show: 80 | api_key = get_api_key() 81 | masked_key = f"{api_key[:4]}...{api_key[-4:]}" if api_key else "Not set" 82 | console.print(f"API Key: {masked_key}") 83 | console.print(f"Max Concurrent Jobs: {config.settings.max_concurrent_jobs}") 84 | console.print(f"Check Interval: {config.settings.check_interval} seconds") 85 | elif reset: 86 | config.settings = BatchWizardSettings() 87 | config.save() 88 | console.print("[yellow]Configuration reset to default values.[/yellow]") 89 | else: 90 | console.print( 91 | "Use --set-key, --show, or --reset options to manage configuration." 92 | ) 93 | 94 | 95 | @app.command() 96 | def list_jobs( 97 | limit: int = typer.Option(100, help="Number of jobs to display"), 98 | all: bool = typer.Option(False, "--all", help="Display all jobs"), 99 | ): 100 | """List recent batch jobs.""" 101 | 102 | async def fetch_jobs(): 103 | processor = BatchProcessor() 104 | console = Console() # Create a Console object directly 105 | try: 106 | jobs = await processor.client.batches.list(limit=None if all else limit) 107 | table = Table(title="Batch Jobs") 108 | table.add_column("Job ID", style="cyan") 109 | table.add_column("Status", style="magenta") 110 | table.add_column("Created At", style="green") 111 | table.add_column("Completed", style="blue") 112 | table.add_column("Failed", style="red") 113 | 114 | for job in jobs.data: 115 | created_at = datetime.fromtimestamp(job.created_at).strftime( 116 | "%Y-%m-%d %H:%M:%S" 117 | ) 118 | table.add_row( 119 | job.id, 120 | job.status, 121 | created_at, 122 | str(job.request_counts.completed), 123 | str(job.request_counts.failed), 124 | ) 125 | console.print(table) # Use the console object to print the table 126 | finally: 127 | await processor.close() 128 | 129 | asyncio.run(fetch_jobs()) 130 | 131 | 132 | @app.command() 133 | def cancel( 134 | job_id: str = typer.Argument(..., help="ID of the batch job to cancel"), 135 | ): 136 | """Cancel a specific batch job.""" 137 | 138 | async def cancel_job(): 139 | processor = BatchProcessor() 140 | try: 141 | await processor.client.batches.cancel(job_id) 142 | console.print(f"[green]Job {job_id} cancelled successfully.[/green]") 143 | except Exception as e: 144 | console.print(f"[red]Error cancelling job {job_id}: {str(e)}[/red]") 145 | finally: 146 | await processor.close() 147 | 148 | asyncio.run(cancel_job()) 149 | 150 | 151 | @app.command() 152 | def download( 153 | job_id: str = typer.Argument( 154 | ..., help="ID of the batch job to download results for" 155 | ), 156 | output_file: Path = typer.Option( 157 | None, help="Path to save the output file (default: _results.jsonl)" 158 | ), 159 | ): 160 | """Download results for a completed batch job.""" 161 | if not output_file: 162 | output_file = Path(f"{job_id}_results.jsonl") 163 | 164 | async def download_results(): 165 | processor = BatchProcessor() 166 | try: 167 | batch_job = await processor.client.batches.retrieve(job_id) 168 | if batch_job.status != "completed": 169 | console.print( 170 | f"[yellow]Job {job_id} is not completed (status: {batch_job.status}). Cannot download results.[/yellow]" 171 | ) 172 | return 173 | 174 | success = await processor.download_batch_results(batch_job, output_file) 175 | if success: 176 | console.print( 177 | f"[green]Results for job {job_id} downloaded successfully to {output_file}[/green]" 178 | ) 179 | else: 180 | console.print(f"[red]Failed to download results for job {job_id}[/red]") 181 | except Exception as e: 182 | console.print( 183 | f"[red]Error downloading results for job {job_id}: {str(e)}[/red]" 184 | ) 185 | finally: 186 | await processor.close() 187 | 188 | asyncio.run(download_results()) 189 | 190 | 191 | if __name__ == "__main__": 192 | app() 193 | -------------------------------------------------------------------------------- /src/batchwizard/config.py: -------------------------------------------------------------------------------- 1 | # config.py 2 | import os 3 | from pathlib import Path 4 | from typing import Optional 5 | 6 | import typer 7 | from pydantic import BaseModel, Field 8 | from pydantic_settings import BaseSettings, SettingsConfigDict 9 | 10 | 11 | class BatchWizardSettings(BaseSettings): 12 | api_key: Optional[str] = Field(None, env="OPENAI_API_KEY") 13 | max_concurrent_jobs: int = 5 14 | check_interval: int = 5 15 | model_config = SettingsConfigDict( 16 | env_file=".env", env_file_encoding="utf-8", extra="ignore" 17 | ) 18 | 19 | 20 | class Config(BaseModel): 21 | settings: BatchWizardSettings = BatchWizardSettings() 22 | 23 | @property 24 | def config_dir(self) -> Path: 25 | return Path(typer.get_app_dir("BatchWizard")) 26 | 27 | @property 28 | def config_file(self) -> Path: 29 | return self.config_dir / "config.json" 30 | 31 | def load(self) -> None: 32 | if self.config_file.exists(): 33 | self.settings = BatchWizardSettings.model_validate_json( 34 | self.config_file.read_text() 35 | ) 36 | 37 | def save(self) -> None: 38 | self.config_dir.mkdir(parents=True, exist_ok=True) 39 | self.config_file.write_text(self.settings.model_dump_json()) 40 | 41 | def get_api_key(self) -> Optional[str]: 42 | return self.settings.api_key or os.getenv("OPENAI_API_KEY") 43 | 44 | def set_api_key(self, api_key: str) -> None: 45 | self.settings.api_key = api_key 46 | self.save() 47 | 48 | 49 | config = Config() 50 | config.load() 51 | -------------------------------------------------------------------------------- /src/batchwizard/models.py: -------------------------------------------------------------------------------- 1 | # models.py 2 | from pathlib import Path 3 | from typing import Optional 4 | 5 | from pydantic import BaseModel 6 | 7 | 8 | class BatchJob(BaseModel): 9 | id: str 10 | status: str 11 | input_file_id: str 12 | output_file_id: Optional[str] = None 13 | 14 | 15 | class BatchJobResult(BaseModel): 16 | job_id: str 17 | success: bool 18 | output_file_path: Optional[Path] = None 19 | -------------------------------------------------------------------------------- /src/batchwizard/processor.py: -------------------------------------------------------------------------------- 1 | # processor.py 2 | import asyncio 3 | from pathlib import Path 4 | from typing import List, Optional 5 | 6 | import aiofiles 7 | from loguru import logger 8 | from openai import AsyncOpenAI 9 | 10 | from .config import config 11 | from .models import BatchJob, BatchJobResult 12 | 13 | 14 | class BatchProcessor: 15 | def __init__(self): 16 | self.client = AsyncOpenAI(api_key=config.get_api_key()) 17 | self.settings = config.settings 18 | 19 | async def upload_file(self, file_path: Path) -> Optional[str]: 20 | try: 21 | async with aiofiles.open(file_path, "rb") as file: 22 | file_content = await file.read() 23 | 24 | response = await self.client.files.create( 25 | file=(file_path.name, file_content), purpose="batch" 26 | ) 27 | logger.info( 28 | f"File uploaded successfully: {response.id}, Filename: {file_path.name}" 29 | ) 30 | return response.id 31 | except Exception as e: 32 | logger.error(f"Error uploading file {file_path.name}: {str(e)}") 33 | return None 34 | 35 | async def create_batch_job(self, input_file_id: str) -> Optional[BatchJob]: 36 | try: 37 | batch_job = await self.client.batches.create( 38 | input_file_id=input_file_id, 39 | endpoint="/v1/chat/completions", 40 | completion_window="24h", 41 | ) 42 | logger.info(f"Created batch job with ID: {batch_job.id}") 43 | return BatchJob( 44 | id=batch_job.id, 45 | status=self.normalize_status(batch_job.status), 46 | input_file_id=input_file_id, 47 | ) 48 | except Exception as e: 49 | logger.error(f"Error creating batch job: {str(e)}") 50 | return None 51 | 52 | async def check_batch_status(self, batch_id: str) -> Optional[str]: 53 | try: 54 | batch_job = await self.client.batches.retrieve(batch_id) 55 | return self.normalize_status(batch_job.status) 56 | except Exception as e: 57 | logger.error(f"Error checking batch status: {str(e)}") 58 | return None 59 | 60 | def normalize_status(self, status: str) -> str: 61 | """Normalize the status string to lowercase with underscores.""" 62 | return status.lower().replace(" ", "_") 63 | 64 | async def download_batch_results(self, batch_job, output_file_path: Path) -> bool: 65 | try: 66 | if batch_job.status == "completed" and batch_job.output_file_id: 67 | result = await self.client.files.content(batch_job.output_file_id) 68 | async with aiofiles.open(output_file_path, "wb") as file: 69 | await file.write(result.content) 70 | logger.info(f"Downloaded results to {output_file_path}") 71 | return True 72 | else: 73 | logger.warning( 74 | f"Batch job not completed or missing output file. Status: {batch_job.status}" 75 | ) 76 | return False 77 | except Exception as e: 78 | logger.error(f"Error downloading batch results: {str(e)}") 79 | return False 80 | 81 | async def process_batch_job( 82 | self, batch_job: BatchJob, output_dir: Path 83 | ) -> BatchJobResult: 84 | check_interval = self.settings.check_interval 85 | while True: 86 | status = await self.check_batch_status(batch_job.id) 87 | if status == "completed": 88 | try: 89 | batch_job = await self.client.batches.retrieve(batch_job.id) 90 | if batch_job.output_file_id: 91 | output_file = output_dir / f"{batch_job.id}_results.jsonl" 92 | if await self.download_batch_results(batch_job, output_file): 93 | logger.info( 94 | f"Successfully processed batch job {batch_job.id}" 95 | ) 96 | return BatchJobResult( 97 | job_id=batch_job.id, 98 | success=True, 99 | output_file_path=output_file, 100 | ) 101 | else: 102 | logger.error( 103 | f"No output file ID found for completed batch job {batch_job.id}" 104 | ) 105 | except Exception as e: 106 | logger.error( 107 | f"Error processing completed batch job {batch_job.id}: {str(e)}" 108 | ) 109 | elif status in ["failed", "expired", "cancelled"]: 110 | logger.error(f"Batch job {batch_job.id} {status}") 111 | return BatchJobResult(job_id=batch_job.id, success=False) 112 | elif status is None: 113 | logger.error(f"Failed to retrieve status for batch job {batch_job.id}") 114 | return BatchJobResult(job_id=batch_job.id, success=False) 115 | 116 | await asyncio.sleep(check_interval) 117 | check_interval = min( 118 | check_interval * 1.5, 60 119 | ) # Implement exponential backoff 120 | 121 | async def process_inputs( 122 | self, input_paths: List[Path], output_dir: Path 123 | ) -> List[BatchJobResult]: 124 | input_files = [] 125 | for path in input_paths: 126 | if path.is_dir(): 127 | input_files.extend(path.glob("*.jsonl")) 128 | elif path.suffix.lower() == ".jsonl": 129 | input_files.append(path) 130 | else: 131 | logger.warning(f"Skipping non-JSONL file: {path}") 132 | 133 | if not input_files: 134 | logger.warning("No input files found in the provided paths") 135 | return [] 136 | 137 | semaphore = asyncio.Semaphore(self.settings.max_concurrent_jobs) 138 | 139 | async def process_file(input_file: Path) -> Optional[BatchJobResult]: 140 | async with semaphore: 141 | file_id = await self.upload_file(input_file) 142 | if file_id: 143 | batch_job = await self.create_batch_job(file_id) 144 | if batch_job: 145 | return await self.process_batch_job(batch_job, output_dir) 146 | return None 147 | 148 | tasks = [process_file(file) for file in input_files] 149 | results = await asyncio.gather(*tasks) 150 | return [result for result in results if result is not None] 151 | 152 | async def close(self): 153 | await self.client.close() 154 | -------------------------------------------------------------------------------- /src/batchwizard/ui.py: -------------------------------------------------------------------------------- 1 | # ui.py 2 | import asyncio 3 | from datetime import datetime 4 | from pathlib import Path 5 | from typing import List 6 | 7 | from loguru import logger 8 | from rich.console import Console 9 | from rich.layout import Layout 10 | from rich.live import Live 11 | from rich.panel import Panel 12 | from rich.progress import (BarColumn, Progress, SpinnerColumn, 13 | TaskProgressColumn, TextColumn, TimeElapsedColumn) 14 | from rich.table import Table 15 | from rich.text import Text 16 | 17 | from .processor import BatchProcessor 18 | 19 | 20 | class BatchWizardUI: 21 | def __init__(self, console: Console): 22 | self.console = console 23 | self.job_table = self.create_job_table() 24 | self.stats_table = self.create_stats_table() 25 | self.log_messages = [] 26 | self.jobs = {} # Dictionary to store job data 27 | self.total_jobs = 0 28 | self.completed_jobs = 0 29 | self.failed_jobs = 0 30 | 31 | def create_layout(self) -> Layout: 32 | layout = Layout() 33 | layout.split_column(Layout(name="header", size=3), Layout(name="body")) 34 | layout["body"].split_row( 35 | Layout(name="main", ratio=2), Layout(name="sidebar", ratio=1) 36 | ) 37 | layout["body"]["main"].split_column( 38 | Layout(name="progress", ratio=1), Layout(name="job_status", ratio=2) 39 | ) 40 | layout["body"]["sidebar"].split_column( 41 | Layout(name="stats", ratio=1), Layout(name="logs", ratio=2) 42 | ) 43 | return layout 44 | 45 | def create_progress_bars(self): 46 | overall_progress = Progress( 47 | SpinnerColumn(), 48 | TextColumn("[progress.description]{task.description}"), 49 | BarColumn(), 50 | TaskProgressColumn(), 51 | TimeElapsedColumn(), 52 | ) 53 | job_progress = Progress( 54 | TextColumn("[progress.description]{task.description}"), 55 | BarColumn(), 56 | TaskProgressColumn(), 57 | ) 58 | return overall_progress, job_progress 59 | 60 | def create_job_table(self): 61 | job_table = Table(show_header=True, header_style="bold magenta", expand=True) 62 | job_table.add_column("Job ID", style="dim", no_wrap=True) 63 | job_table.add_column("Status", style="bold") 64 | job_table.add_column("Progress", justify="right") 65 | return job_table 66 | 67 | def update_job_status(self, job_id: str, status: str, progress: str): 68 | color = ( 69 | "green" 70 | if status == "completed" 71 | else "red" if status in ["failed", "expired", "cancelled"] else "yellow" 72 | ) 73 | self.jobs[job_id] = (f"[{color}]{status}", progress) 74 | self.job_table = self.create_job_table() 75 | for job_id, (status, progress) in self.jobs.items(): 76 | self.job_table.add_row(job_id, status, progress) 77 | 78 | def create_stats_table(self): 79 | stats_table = Table(show_header=False, expand=True) 80 | stats_table.add_column("Metric", style="cyan") 81 | stats_table.add_column("Value", justify="right") 82 | stats_table.add_row("Total Jobs", "0") 83 | stats_table.add_row("Completed", "0") 84 | stats_table.add_row("In Progress", "0") 85 | stats_table.add_row("Failed", "0") 86 | return stats_table 87 | 88 | def update_stats(self): 89 | self.stats_table.rows.clear() 90 | self.stats_table.add_row("Total Jobs", str(self.total_jobs)) 91 | self.stats_table.add_row("Completed", f"[green]{self.completed_jobs}[/green]") 92 | self.stats_table.add_row( 93 | "In Progress", 94 | f"[yellow]{self.total_jobs - self.completed_jobs - self.failed_jobs}[/yellow]", 95 | ) 96 | self.stats_table.add_row("Failed", f"[red]{self.failed_jobs}[/red]") 97 | 98 | def add_log(self, message: str): 99 | self.log_messages.append(message) 100 | if len(self.log_messages) > 10: # Keep only the last 10 log messages 101 | self.log_messages.pop(0) 102 | logger.info(message) 103 | 104 | def get_log_panel(self): 105 | return Panel("\n".join(self.log_messages), title="Logs", border_style="green") 106 | 107 | def update_layout( 108 | self, 109 | layout: Layout, 110 | header: str, 111 | overall_progress: Progress, 112 | job_table: Table, 113 | stats_table: Table, 114 | log_panel: Panel, 115 | ): 116 | layout["header"].update( 117 | Panel(Text(header, style="bold blue"), border_style="blue") 118 | ) 119 | layout["body"]["main"]["progress"].update( 120 | Panel(overall_progress, title="Overall Progress", border_style="green") 121 | ) 122 | layout["body"]["main"]["job_status"].update( 123 | Panel(job_table, title="Job Status", border_style="magenta") 124 | ) 125 | layout["body"]["sidebar"]["stats"].update( 126 | Panel(stats_table, title="Statistics", border_style="cyan") 127 | ) 128 | layout["body"]["sidebar"]["logs"].update(log_panel) 129 | 130 | async def run_processing( 131 | self, processor: BatchProcessor, input_paths: List[Path], output_dir: Path 132 | ): 133 | layout = self.create_layout() 134 | overall_progress, job_progress = self.create_progress_bars() 135 | 136 | overall_task = overall_progress.add_task("[bold blue]Processing", total=100) 137 | upload_task = overall_progress.add_task("[green]Uploading files", visible=False) 138 | process_task = overall_progress.add_task("[cyan]Processing jobs", visible=False) 139 | 140 | async def update_ui(): 141 | self.update_layout( 142 | layout, 143 | "BatchWizard Processing", 144 | overall_progress, 145 | self.job_table, 146 | self.stats_table, 147 | self.get_log_panel(), 148 | ) 149 | 150 | with Live(layout, console=self.console, screen=True, refresh_per_second=4): 151 | await update_ui() 152 | 153 | input_files = [] 154 | for path in input_paths: 155 | if path.is_dir(): 156 | input_files.extend(path.glob("*.jsonl")) 157 | elif path.suffix.lower() == ".jsonl": 158 | input_files.append(path) 159 | else: 160 | self.add_log(f"Skipping non-JSONL file: {path}") 161 | 162 | if not input_files: 163 | self.add_log("No input files found in the provided paths") 164 | await update_ui() 165 | return 166 | 167 | overall_progress.update(upload_task, total=len(input_files), visible=True) 168 | overall_progress.update(process_task, total=len(input_files), visible=True) 169 | 170 | self.total_jobs = len(input_files) 171 | 172 | async def process_file(input_file: Path): 173 | file_id = await processor.upload_file(input_file) 174 | overall_progress.update(upload_task, advance=1) 175 | if file_id: 176 | batch_job = await processor.create_batch_job(file_id) 177 | if batch_job: 178 | self.update_job_status(batch_job.id, batch_job.status, "0%") 179 | self.add_log(f"Job created: {batch_job.id}") 180 | await update_ui() 181 | 182 | result = await processor.process_batch_job( 183 | batch_job, output_dir 184 | ) 185 | if result.success: 186 | self.completed_jobs += 1 187 | self.update_job_status(batch_job.id, "completed", "100%") 188 | self.add_log(f"Job completed: {batch_job.id}") 189 | if result.output_file_path: 190 | self.add_log( 191 | f"Results saved to: {result.output_file_path}" 192 | ) 193 | else: 194 | self.failed_jobs += 1 195 | self.update_job_status(batch_job.id, "failed", "100%") 196 | self.add_log(f"Job failed: {batch_job.id}") 197 | 198 | overall_progress.update(process_task, advance=1) 199 | self.update_stats() 200 | await update_ui() 201 | else: 202 | self.add_log( 203 | f"Failed to create batch job for {input_file.name}" 204 | ) 205 | else: 206 | self.add_log(f"Failed to upload file {input_file.name}") 207 | await update_ui() 208 | 209 | tasks = [process_file(file) for file in input_files] 210 | await asyncio.gather(*tasks) 211 | 212 | overall_progress.update(overall_task, completed=100) 213 | await update_ui() 214 | 215 | self.console.print("[bold green]Processing completed![/bold green]") 216 | self.console.print(f"Total jobs: {self.total_jobs}") 217 | self.console.print(f"Completed jobs: {self.completed_jobs}") 218 | self.console.print(f"Failed jobs: {self.failed_jobs}") 219 | if self.completed_jobs > 0: 220 | self.console.print(f"Results saved in: {output_dir}") 221 | 222 | def display_job_list(self, jobs: List[dict]): 223 | table = Table(title="Batch Jobs") 224 | table.add_column("Job ID", style="cyan") 225 | table.add_column("Status", style="magenta") 226 | table.add_column("Created At", style="green") 227 | table.add_column("Completed", style="blue") 228 | table.add_column("Failed", style="red") 229 | 230 | for job in jobs: 231 | created_at = datetime.fromtimestamp(job["created_at"]).strftime( 232 | "%Y-%m-%d %H:%M:%S" 233 | ) 234 | table.add_row( 235 | job["id"], 236 | job["status"], 237 | created_at, 238 | str(job["request_counts"]["completed"]), 239 | str(job["request_counts"]["failed"]), 240 | ) 241 | 242 | self.console.print(table) 243 | 244 | def display_cancel_result(self, job_id: str, success: bool): 245 | if success: 246 | self.console.print(f"[green]Job {job_id} cancelled successfully.[/green]") 247 | else: 248 | self.console.print(f"[red]Failed to cancel job {job_id}.[/red]") 249 | 250 | def display_download_result(self, job_id: str, output_file: Path, success: bool): 251 | if success: 252 | self.console.print( 253 | f"[green]Results for job {job_id} downloaded successfully to {output_file}[/green]" 254 | ) 255 | else: 256 | self.console.print( 257 | f"[red]Failed to download results for job {job_id}[/red]" 258 | ) 259 | -------------------------------------------------------------------------------- /src/batchwizard/utils.py: -------------------------------------------------------------------------------- 1 | # utils.py 2 | from typing import Optional 3 | 4 | from loguru import logger 5 | from rich.console import Console 6 | 7 | from .config import config 8 | 9 | 10 | def setup_logger(console: Console = None): 11 | logger.remove() 12 | if console is None: 13 | console = Console() 14 | logger.add( 15 | console.file, 16 | format="{time:YYYY-MM-DD HH:mm:ss} | {level: <8} | {name}:{function}:{line} - {message}", 17 | level="ERROR", 18 | backtrace=True, 19 | diagnose=True, 20 | ) 21 | return logger 22 | 23 | 24 | def get_api_key() -> Optional[str]: 25 | """Get the API key from config or environment variable.""" 26 | return config.get_api_key() 27 | 28 | 29 | def set_api_key(api_key: str) -> None: 30 | """Set the API key in the config.""" 31 | config.set_api_key(api_key) 32 | 33 | 34 | def get_settings(): 35 | """Get the current settings.""" 36 | return config.settings 37 | --------------------------------------------------------------------------------