├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .env ├── .gitignore ├── .markdown-link.json ├── .markdownlint-cli2.jsonc ├── .package-template ├── .amlignore ├── README.md ├── aml-job.yaml ├── pyproject.toml ├── src │ └── package_template │ │ ├── __init__.py │ │ ├── __main__.py │ │ └── answer.py └── tests │ └── test_answer.py ├── .pipeline-template ├── example-reader-step │ ├── __main__.py │ └── aml-component.yaml ├── example-writer-step │ ├── __main__.py │ └── aml-component.yaml └── pipeline-template.yaml ├── .pre-commit-config.yaml ├── .vscode └── settings.json ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── bin ├── README.md ├── chkenv ├── data │ ├── download │ ├── find │ ├── list │ └── register ├── dev │ ├── sync │ └── test ├── env ├── help ├── lib │ ├── _aml │ ├── _prep-exp │ ├── run.sh │ └── utils.sh ├── lint │ ├── md │ ├── py │ └── shell ├── pipe │ ├── _iso │ ├── aml │ ├── local │ └── new └── pkg │ ├── _iso │ ├── aml │ ├── local │ ├── new │ └── rm ├── data └── .gitignore ├── notebooks └── .gitignore ├── packages ├── example-reader-step │ ├── .amlignore │ ├── README.md │ ├── aml-component.yaml │ ├── environment │ │ └── Dockerfile │ ├── pyproject.toml │ └── src │ │ └── example_reader_step │ │ ├── __init__.py │ │ └── __main__.py ├── example-writer-step │ ├── .amlignore │ ├── README.md │ ├── aml-component.yaml │ ├── environment │ │ └── Dockerfile │ ├── pyproject.toml │ └── src │ │ └── example_writer_step │ │ ├── __init__.py │ │ └── __main__.py ├── example │ ├── .amlignore │ ├── README.md │ ├── aml-job.yaml │ ├── environment │ │ └── Dockerfile │ ├── pyproject.toml │ ├── src │ │ └── example │ │ │ ├── __init__.py │ │ │ ├── __main__.py │ │ │ └── answer.py │ └── tests │ │ └── test_answer.py └── shared │ ├── README.md │ ├── pyproject.toml │ ├── src │ └── shared │ │ ├── __init__.py │ │ └── logging │ │ └── azureml_logger.py │ └── tests │ └── __init__.py ├── pipelines └── example-pipeline.yaml ├── pyproject.toml ├── runs └── .gitignore └── uv.lock /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:jammy AS base 2 | 3 | ARG PYTHON_VERSION=3.12 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | 9 | ENV UV_LINK_MODE="copy" 10 | ENV UV_PYTHON=${PYTHON_VERSION} 11 | COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /bin/ 12 | RUN uv python install ${PYTHON_VERSION} 13 | 14 | 15 | # Stage only used for development 16 | FROM base AS devcontainer 17 | 18 | # Install the Microsoft Linux Repository 19 | RUN curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb && \ 20 | sudo dpkg -i packages-microsoft-prod.deb && \ 21 | rm packages-microsoft-prod.deb && \ 22 | sudo apt-get update 23 | 24 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 25 | && apt-get -y install --no-install-recommends \ 26 | shellcheck \ 27 | azcopy 28 | 29 | 30 | # Stage used in AML. AzureML always builds last stage. 31 | FROM base AS job-runner 32 | 33 | # non-root user, created on base image 34 | USER vscode 35 | ARG VENV_PATH=/home/vscode/venv 36 | 37 | # disable caching as we build once 38 | ENV UV_NO_CACHE=1 39 | 40 | # Create a virtual environment 41 | RUN uv venv ${VENV_PATH} 42 | # Use the virtual environment automatically 43 | ENV VIRTUAL_ENV="${VENV_PATH}" 44 | # Ensure all commands use the virtual environment 45 | ENV PATH="${VENV_PATH}/bin:$PATH" 46 | 47 | # Expect requirements.txt in context for running AzureML jobs 48 | COPY requirements.txt . 49 | RUN uv pip install -r requirements.txt 50 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "scaffolding", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "target": "devcontainer" 6 | }, 7 | "workspaceFolder": "/workspaces/${localWorkspaceFolderBasename}", 8 | "hostRequirements": { 9 | "gpu": "optional" // Ensure access to GPU if host has some 10 | }, 11 | "runArgs": [ 12 | "--ipc", "host" // Needed for heavy torch training, harmless otherwise 13 | ], 14 | "remoteEnv": { 15 | // Ensure repo env is always first in PATH 16 | "PATH": "${containerWorkspaceFolder}/.venv/bin:${containerEnv:PATH}", 17 | // Make root path easily discoverable 18 | "REPO_ROOT": "${containerWorkspaceFolder}" 19 | }, 20 | "remoteUser": "vscode", 21 | "customizations": { 22 | "vscode": { 23 | "extensions": [ 24 | "ms-python.python", 25 | "ms-toolsai.jupyter", 26 | "GitHub.copilot", 27 | "charliermarsh.ruff", 28 | "tamasfe.even-better-toml", 29 | "jeff-hykin.better-dockerfile-syntax", 30 | "stkb.rewrap", 31 | "davidanson.vscode-markdownlint", 32 | "timonwong.shellcheck" 33 | ] 34 | } 35 | }, 36 | "features": { 37 | "ghcr.io/devcontainers/features/azure-cli:1": { 38 | "extensions": "ml" 39 | }, 40 | // used for markdown linting 41 | "ghcr.io/devcontainers/features/node:1": {} 42 | }, 43 | "postCreateCommand": "bin/dev/sync" 44 | } -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | ################################################################ 2 | # Configuration file used by scripts to work with your project # 3 | ################################################################ 4 | 5 | # Tenant ID of the Azure subscription 6 | TENANT_ID= 7 | # Azure Machine Learning workspace scripts point to 8 | AZUREML_WORKSPACE= 9 | # Azure Machine Learning resource group scripts point to 10 | AZUREML_RESOURCE_GROUP= 11 | # Folder where packages live 12 | PKGS_PATH=./packages 13 | # Folder where pipelines live 14 | PIPES_PATH=./pipelines 15 | # Relative folder where runs are isolated before submitting to AzureML 16 | RUNS_PATH=./runs 17 | # Branch to use for experiment tracking 18 | EXPERIMENTS_BRANCH=experiments 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Requirements are generated dynamically on submission, no need to track them 2 | requirements.txt 3 | 4 | # Local entrypoints should be developer-specific 5 | **/debug.py 6 | 7 | # Byte-compiled / optimized / DLL files 8 | __pycache__/ 9 | *.py[cod] 10 | *$py.class 11 | 12 | # C extensions 13 | *.so 14 | 15 | # Distribution / packaging 16 | .Python 17 | build/ 18 | develop-eggs/ 19 | dist/ 20 | downloads/ 21 | eggs/ 22 | .eggs/ 23 | lib64/ 24 | parts/ 25 | sdist/ 26 | var/ 27 | wheels/ 28 | pip-wheel-metadata/ 29 | share/python-wheels/ 30 | *.egg-info/ 31 | .installed.cfg 32 | *.egg 33 | MANIFEST 34 | 35 | # PyInstaller 36 | # Usually these files are written by a python script from a template 37 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 38 | *.manifest 39 | *.spec 40 | 41 | # Installer logs 42 | pip-log.txt 43 | pip-delete-this-directory.txt 44 | 45 | # Unit test / coverage reports 46 | htmlcov/ 47 | .tox/ 48 | .nox/ 49 | .coverage 50 | .coverage.* 51 | .cache 52 | nosetests.xml 53 | coverage.xml 54 | *.cover 55 | *.py,cover 56 | .hypothesis/ 57 | .pytest_cache/ 58 | 59 | # Translations 60 | *.mo 61 | *.pot 62 | 63 | # Django stuff: 64 | *.log 65 | local_settings.py 66 | db.sqlite3 67 | db.sqlite3-journal 68 | 69 | # Flask stuff: 70 | instance/ 71 | .webassets-cache 72 | 73 | # Scrapy stuff: 74 | .scrapy 75 | 76 | # Sphinx documentation 77 | docs/_build/ 78 | 79 | # PyBuilder 80 | target/ 81 | 82 | # Jupyter Notebook 83 | .ipynb_checkpoints 84 | 85 | # IPython 86 | profile_default/ 87 | ipython_config.py 88 | 89 | # pyenv 90 | .python-version 91 | 92 | # pipenv 93 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 94 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 95 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 96 | # install all needed dependencies. 97 | #Pipfile.lock 98 | 99 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 100 | __pypackages__/ 101 | 102 | # Celery stuff 103 | celerybeat-schedule 104 | celerybeat.pid 105 | 106 | # Ruff stuff 107 | .ruff_cache 108 | 109 | # SageMath parsed files 110 | *.sage.py 111 | 112 | # Environments 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | .env.local 120 | 121 | # Spyder project settings 122 | .spyderproject 123 | .spyproject 124 | 125 | # Rope project settings 126 | .ropeproject 127 | 128 | # mkdocs documentation 129 | /site 130 | 131 | # mypy 132 | .mypy_cache/ 133 | .dmypy.json 134 | dmypy.json 135 | 136 | # Pyre type checker 137 | .pyre/ 138 | 139 | # macOS stuff 140 | **/.DS_Store 141 | 142 | # IDE stuff 143 | .idea/* 144 | -------------------------------------------------------------------------------- /.markdown-link.json: -------------------------------------------------------------------------------- 1 | { 2 | "projectBaseUrl":"${workspaceFolder}", 3 | "ignorePatterns": [], 4 | "replacementPatterns": [ 5 | { 6 | "pattern": "^/", 7 | "replacement": "{{BASEURL}}/" 8 | }, 9 | { 10 | "pattern": "%20", 11 | "replacement": "-", 12 | "global": true 13 | } 14 | ], 15 | "timeout": "5s", 16 | "retryOn429": true, 17 | "retryCount": 2, 18 | "fallbackRetryDelay": "5s", 19 | "aliveStatusCodes": [200, 203, 206] 20 | } 21 | -------------------------------------------------------------------------------- /.markdownlint-cli2.jsonc: -------------------------------------------------------------------------------- 1 | // MD013 is for line length control 2 | // For readability wrapping, tables and code blocks are excluded by default 3 | { 4 | "config": { 5 | "MD013": { 6 | "tables": false, 7 | "code_blocks": false, 8 | "line_length": 80 9 | } 10 | }, 11 | "showFound": false, 12 | "ignores": [ 13 | "**/node_modules/**", 14 | ".github", 15 | ".venv", 16 | "tmp", 17 | "runs" 18 | ], 19 | "globs": ["**.md"] 20 | } 21 | -------------------------------------------------------------------------------- /.package-template/.amlignore: -------------------------------------------------------------------------------- 1 | # To prevent unnecessary files from being included in 2 | # the AzureML code snapshot, make an ignore file (.amlignore). 3 | # Place this file in the Snapshot directory and add the 4 | # filenames to ignore in it. The .amlignore file uses 5 | # the same syntax and patterns as the .gitignore file. 6 | # If no .amlignore file is present, the .gitignore file 7 | # is used to determine which files to ignore. 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # Unit test cache 15 | .pytest_cache/ 16 | 17 | # macOS stuff 18 | **/.DS_Store -------------------------------------------------------------------------------- /.package-template/README.md: -------------------------------------------------------------------------------- 1 | # package_template 2 | -------------------------------------------------------------------------------- /.package-template/aml-job.yaml: -------------------------------------------------------------------------------- 1 | # Tells Azure ML what kind of YAML this is. 2 | # Docs: https://docs.microsoft.com/en-us/azure/machine-learning/reference-yaml-job-command 3 | $schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json 4 | 5 | # Name of the experiment where all jobs will end up in the Azure ML dashboard 6 | experiment_name: package_template 7 | 8 | # What to run 9 | command: >- 10 | python -m package_template 11 | --data_path "${{inputs.data_path}}" 12 | --greeting "${{inputs.greeting}}" 13 | 14 | inputs: 15 | data_path: 16 | # Only works if dataset created using Azure ML CLI v2; run `az ml data create --help` to see how 17 | # YAML schema: https://learn.microsoft.com/en-us/azure/machine-learning/reference-yaml-data 18 | type: uri_folder # default, can be changed to `uri_file` if data_path points to a file 19 | path: azureml:: 20 | greeting: "Hello" 21 | 22 | # What code to make available 23 | code: . 24 | 25 | # Where to run it 26 | environment: 27 | build: 28 | path: ./environment 29 | 30 | compute: azureml: 31 | -------------------------------------------------------------------------------- /.package-template/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "package-template" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | dependencies = [ 7 | # needed for logging in AzureML 8 | "azureml-mlflow==1.60.*", 9 | "mlflow-skinny==2.21.*", 10 | "pytz==2025.2", 11 | ] 12 | 13 | [project.scripts] 14 | package-template = "package_template.__main__:main" 15 | 16 | [build-system] 17 | requires = ["hatchling"] 18 | build-backend = "hatchling.build" 19 | -------------------------------------------------------------------------------- /.package-template/src/package_template/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bepuca/azureml-scaffolding/d962edbd2edce2fe7c493be31e05e5ca4352c1bb/.package-template/src/package_template/__init__.py -------------------------------------------------------------------------------- /.package-template/src/package_template/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | from package_template.answer import get_the_ultimate_answer 5 | from shared.logging import azureml_logger 6 | 7 | 8 | def example(data_path: Path, greeting: str = "Hello"): 9 | """Example function with the main things needed to get started with AzureML 10 | 11 | :param data_path: Path where data is stored. Here to exemplify how to connect AzureML data 12 | (see `aml-job.yaml`). 13 | :param greeting: Word with which to greet the world. 14 | """ 15 | 16 | # Tags are shown as properties of the job in the Azure ML dashboard. Run once. 17 | # Good practice to set input values as tags for reproducibility 18 | tags = {"greeting": greeting} 19 | azureml_logger.set_tags(tags) 20 | 21 | # Console logs in AzureML are captured from stdout and stderr 22 | print(f"{greeting} world!") 23 | print(f"'data_path' is pointing to '{data_path}'", file=sys.stderr) 24 | if isinstance(data_path, Path) and data_path.is_dir(): 25 | for file in data_path.iterdir(): 26 | print(f" {file.relative_to(data_path)}", file=sys.stderr) 27 | 28 | # Metrics are numerical values 29 | metrics = {"answer": get_the_ultimate_answer()} 30 | azureml_logger.log_metrics(metrics) 31 | 32 | values = [1.0, 0.0, 1.0, 2.0, 3.0, 2.0, 4.0] 33 | for v in values: 34 | metrics = {"value": v} 35 | azureml_logger.log_metrics(metrics) 36 | 37 | 38 | def main(): 39 | import argparse 40 | import sys 41 | from pathlib import Path 42 | 43 | assume_debug = len(sys.argv) <= 1 44 | if assume_debug: 45 | print("WARNING: Using debug args because no args were passed") 46 | args_dict = { 47 | "data_path": Path("path/to/data"), 48 | "greeting": "Hello", 49 | } 50 | else: 51 | parser = argparse.ArgumentParser(description="Driver script for Example Package") 52 | parser.add_argument("--data_path", type=Path, help="Path to data", required=True) 53 | parser.add_argument("--greeting", type=str, help="Greeting word", required=True) 54 | 55 | args = parser.parse_args() 56 | args_dict = vars(args) 57 | 58 | example(**args_dict) 59 | 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /.package-template/src/package_template/answer.py: -------------------------------------------------------------------------------- 1 | def get_the_ultimate_answer() -> float: 2 | """Dummy function to exemplify unit tests""" 3 | return 42.0 4 | -------------------------------------------------------------------------------- /.package-template/tests/test_answer.py: -------------------------------------------------------------------------------- 1 | from package_template.answer import get_the_ultimate_answer 2 | 3 | 4 | def test_get_the_ultimate_answer(): 5 | expected_answer = 42 6 | 7 | answer = get_the_ultimate_answer() 8 | 9 | assert answer == expected_answer 10 | -------------------------------------------------------------------------------- /.pipeline-template/example-reader-step/__main__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def read(data_path: Path): 5 | """Read "file.txt" in `data_path` and print its contents.""" 6 | file = data_path / "file.txt" 7 | print(file.read_text()) 8 | 9 | 10 | def main(): 11 | import argparse 12 | import os 13 | import sys 14 | 15 | assume_debug = len(sys.argv) <= 1 16 | if assume_debug: 17 | print("WARNING: Using debug args because no args were passed") 18 | args_dict = { 19 | "data_path": Path(os.environ["REPO_ROOT"]) / "data/outputs", 20 | } 21 | else: 22 | parser = argparse.ArgumentParser(description="Driver script for Example Package") 23 | parser.add_argument("--data_path", type=Path, help="Path to data", required=True) 24 | 25 | args = parser.parse_args() 26 | args_dict = vars(args) 27 | 28 | read(**args_dict) 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /.pipeline-template/example-reader-step/aml-component.yaml: -------------------------------------------------------------------------------- 1 | $schema: https://azuremlschemas.azureedge.net/latest/commandComponent.schema.json 2 | type: command 3 | 4 | name: example_reader_step 5 | display_name: Example Reader Step 6 | 7 | # What to run (always cd into src for imports to work) 8 | command: >- 9 | cd src && 10 | python -m example_reader_step 11 | --data_path "${{inputs.data_path}}" 12 | 13 | inputs: 14 | data_path: 15 | type: uri_folder 16 | 17 | # What code to make available 18 | code: . 19 | 20 | # Where to run it 21 | environment: 22 | build: 23 | path: ./environment 24 | 25 | -------------------------------------------------------------------------------- /.pipeline-template/example-writer-step/__main__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write(content: str, data_path: Path): 5 | """Write `content` to "file.txt" in `data_path`""" 6 | data_path.mkdir(parents=True, exist_ok=True) 7 | file = data_path / "file.txt" 8 | file.write_text(content) 9 | 10 | 11 | def main(): 12 | import argparse 13 | import os 14 | import sys 15 | 16 | assume_debug = len(sys.argv) <= 1 17 | if assume_debug: 18 | print("WARNING: Using debug args because no args were passed") 19 | args_dict = { 20 | "content": "Houston we do not have a problem!", 21 | "data_path": Path(os.environ["REPO_ROOT"]) / "data/outputs", 22 | } 23 | else: 24 | parser = argparse.ArgumentParser(description="Driver script for Example Package") 25 | parser.add_argument("--content", type=str, help="Content to write", required=True) 26 | parser.add_argument("--data_path", type=Path, help="Path to data", required=True) 27 | 28 | args = parser.parse_args() 29 | args_dict = vars(args) 30 | 31 | write(**args_dict) 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /.pipeline-template/example-writer-step/aml-component.yaml: -------------------------------------------------------------------------------- 1 | $schema: https://azuremlschemas.azureedge.net/latest/commandComponent.schema.json 2 | type: command 3 | 4 | name: example_writer_step 5 | display_name: Example Writer Step 6 | 7 | # What to run (always cd into src for imports to work) 8 | command: >- 9 | cd src && 10 | python -m example_writer_step 11 | --content "${{ inputs.content }}" 12 | --data_path "${{ outputs.data_path }}" 13 | 14 | inputs: 15 | content: 16 | type: string 17 | 18 | outputs: 19 | data_path: 20 | type: uri_folder 21 | 22 | # What code to make available 23 | code: . 24 | 25 | # Where to run it 26 | environment: 27 | build: 28 | path: ./environment 29 | -------------------------------------------------------------------------------- /.pipeline-template/pipeline-template.yaml: -------------------------------------------------------------------------------- 1 | $schema: https://azuremlschemas.azureedge.net/latest/pipelineJob.schema.json 2 | type: pipeline 3 | 4 | experiment_name: pipeline-template 5 | settings: 6 | default_compute: azureml: 7 | 8 | inputs: 9 | content: "hello world" 10 | 11 | jobs: 12 | # always keep the snapshot step so pipeline YAML makes it into the jobs 13 | snapshot: 14 | name: Snapshot 15 | command: echo "Uploading full pipeline snapshot" 16 | code: . 17 | environment: 18 | image: mcr.microsoft.com/azureml/inference-base-2204 19 | 20 | example_writer_step: 21 | type: command 22 | component: ./example-writer-step/aml-component.yaml 23 | inputs: 24 | content: ${{ parent.inputs.content }} 25 | outputs: 26 | data_path: 27 | type: uri_folder 28 | mode: upload 29 | 30 | example_reader_step: 31 | type: command 32 | component: ./example-reader-step/aml-component.yaml 33 | inputs: 34 | data_path: ${{ parent.jobs.example_writer_step.outputs.data_path }} -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: local 3 | hooks: 4 | - id: shellcheck 5 | name: Lint Bash 6 | entry: bin/lint/shell 7 | args: [check] 8 | language: script 9 | files: ^.*\.sh|^bin/ 10 | exclude: ^.*\.yaml|^.env.*|.*\.md 11 | - id: python 12 | name: Lint Python 13 | entry: bin/lint/py 14 | language: system 15 | files: ^.*\.py 16 | - id: md 17 | name: Lint Markdown 18 | entry: bin/lint/md 19 | args: [--fix] 20 | language: script 21 | types: [ text , markdown ] 22 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | // editor settings 3 | "editor.rulers": [ { "column": 80 }, { "column": 100 } ], 4 | 5 | // python settings 6 | "[python]": { "editor.defaultFormatter": "charliermarsh.ruff" }, 7 | "python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python", 8 | "python.envFile": "${workspaceFolder}/.env", 9 | "python.testing.unittestEnabled": false, 10 | "python.testing.pytestEnabled": true, 11 | "python.testing.pytestArgs": ["packages"], 12 | 13 | // shellcheck settings 14 | "shellcheck.customArgs": ["-x", "-s", "bash"], 15 | "shellcheck.ignorePatterns": {"**/*.amlignore": true, "**/*.env.*": true} 16 | } 17 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | After changes, the following workflow should work seamlessly: 4 | 5 | 1. `bin/pkg/new exp_test` 6 | 2. Run local in VSCode. 7 | 3. Run tests in VSCode. 8 | 9 | 4. The following should cause no git changes 10 | 11 | ```bash 12 | bin/pkg/rm example-reader-step && \ 13 | bin/pkg/rm example-writer-step && \ 14 | rm pipelines/example-pipeline.yaml \ 15 | && bin/pipe/new example-pipeline 16 | ``` 17 | 18 | Removing an experiment 19 | 20 | TODO: 21 | run job in aml to test all works nice 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Bernat Puig Camps 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 | # AzureML Scaffolding 2 | 3 | AzureML Scaffolding provides a minimal yet comprehensive template for AI and 4 | Machine Learning projects built on [Azure Machine Learning] (also referred to 5 | as AzureML or AML). It implements [Hypothesis Driven Development] principles to 6 | accelerate experimentation-driven progress. By distilling best practices from 7 | numerous successful projects, we've created a developer-friendly foundation that 8 | eliminates common setup hurdles. This scaffolding is designed to work seamlessly 9 | with AzureML's experiment tracking and compute management, ensuring these 10 | features are leveraged effectively from day one. Get started in minutes and 11 | focus on what matters: building and shipping valuable ML solutions through 12 | efficient experimentation and iteration. 13 | 14 | [Hypothesis Driven Development]: https://www.bepuca.dev/posts/hdd-for-ai/ 15 | [Azure Machine Learning]: 16 | https://learn.microsoft.com/en-us/azure/machine-learning/ 17 | 18 | ## Table of Contents 19 | 20 | - [Why Azure Machine Learning?](#why-azure-machine-learning) 21 | - [Core Design Principles](#core-design-principles) 22 | - [Key Capabilities](#key-capabilities) 23 | - [Prerequisites](#prerequisites) 24 | - [Installation](#installation) 25 | - [Quickstart](#quickstart) 26 | - [Usage](#usage) 27 | - [Data](#data) 28 | - [Registering data](#registering-data) 29 | - [Downloading data](#downloading-data) 30 | - [Packages](#packages) 31 | - [Shared package](#shared-package) 32 | - [Creating a new package](#creating-a-new-package) 33 | - [Adding dependencies to a package](#adding-dependencies-to-a-package) 34 | - [Executing a package](#executing-a-package) 35 | - [Pipelines](#pipelines) 36 | - [Creating a new pipeline](#creating-a-new-pipeline) 37 | - [Executing a pipeline](#executing-a-pipeline) 38 | - [Linting](#linting) 39 | - [Testing](#testing) 40 | 41 | ## Why Azure Machine Learning? 42 | 43 | [Back to the Top](#table-of-contents) 44 | 45 | Azure Machine Learning provides essential capabilities for enterprise ML 46 | development that this scaffolding leverages: 47 | 48 | - **Compute on Demand** - Use powerful machines only when needed and pay for 49 | actual usage. Scale from local development to multi-node clusters without code 50 | changes. 51 | - **Experiment Tracking** - Every run is automatically tracked with metrics, 52 | parameters, and environment specs. Compare experiments, visualize progress, 53 | and identify winning approaches efficiently. 54 | - **Complete Reproducibility** - All code, data references, and environment 55 | definitions are captured in self-contained snapshots. Any experiment can be 56 | reproduced exactly, even months later. 57 | - **Enterprise-grade Data Management** - Leverage Data Assets for versioning, 58 | access control, and lineage tracking of datasets throughout your project 59 | lifecycle. 60 | - **Collaboration** - Share experiments, results, and models with teammates 61 | without needing to share execution environments. Everyone with workspace 62 | access can view runs and inspect outcomes. 63 | 64 | ## Core Design Principles 65 | 66 | [Back to the Top](#table-of-contents) 67 | 68 | This scaffolding was built around four key principles that guide how ML projects 69 | should operate: 70 | 71 | - **Continuous Experimentation** - Conduct multiple parallel experiments without 72 | interference. Each experiment is isolated, allowing for fearless innovation 73 | with clear boundaries between variations. 74 | - **Minimum Viable Compute** - Develop locally, test on small datasets, then 75 | scale to production workloads—all using the same code. Your laptop, a powerful 76 | VM, or a GPU cluster all run identical executions. 77 | - **Developer Experience First** - Clear project structure, simple commands, and 78 | minimal boilerplate let you focus on ML code instead of infrastructure. 79 | Linting, testing, and best practices are built in. 80 | - **Extensibility** - Customize and extend the scaffolding to fit your specific 81 | project needs. The structure accommodates different ML workflows while 82 | maintaining consistency. 83 | 84 | ## Key Capabilities 85 | 86 | [Back to the Top](#table-of-contents) 87 | 88 | AzureML Scaffolding enables you to: 89 | 90 | - **Manage Parallel Experiments** - Develop and run multiple experiments with 91 | independent configurations and clear isolation boundaries. 92 | - **Share Code Efficiently** - Use the `shared` package for code reuse across 93 | packages without duplication. 94 | - **Run Anywhere** - Run the same code locally for fast iteration and remotely 95 | for full-scale execution. Docker containers ensure environment consistency. 96 | - **Structure Your Project** - Maintain clean separation between ML code and 97 | infrastructure code with standardized package organization. 98 | - **Leverage DevOps Practices** - Utilize built-in code linting, formatting, and 99 | testing capabilities to maintain quality. 100 | - **Simplify Workflows** - Access all common tasks through an intuitive CLI with 101 | commands like `bin/pkg/aml` to run experiments in AzureML. 102 | - **Build Pipelines** - Create multi-step ML workflows with proper dependency 103 | management between components. 104 | 105 | ## Prerequisites 106 | 107 | - **Azure ML workspace** - For running and tracking experiments in AzureML, you 108 | need to have an Azure subscription with an Azure ML workspace. The workspace 109 | should have at least one compute cluster defined. 110 | - **Docker** - This project leverages Docker-based environments to maximize 111 | reproducibility. Therefore, you need the [Docker engine] in your machine and 112 | potentially a license for it. 113 | - **VSCode Dev Containers** - We use the [devcontainers] to capitalize on the 114 | Docker environments for an easy-to-set-up and portable development 115 | environment. This project is designed to be used within VSCode [Dev Containers 116 | extension] but it may be possible to tweak it for other editors. 117 | 118 | [Docker engine]: https://docs.docker.com/engine/ 119 | [devcontainers]: https://containers.dev/ 120 | [Dev Containers extension]: 121 | https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers 122 | 123 | ## Installation 124 | 125 | [Back to the Top](#table-of-contents) 126 | 127 | 1. From your project folder, download all files in this repository. **The 128 | following command will overwrite any existing files matching the names of 129 | those in this repository.** It is recommended to run the following only on 130 | a git repository with all changes committed: 131 | 132 | ```bash 133 | curl -L https://github.com/bepuca/azureml-scaffolding/archive/refs/heads/main.zip -o temp.zip \ 134 | && unzip -o temp.zip \ 135 | && cp -a azureml-scaffolding-main/. . \ 136 | && rm -rf temp.zip azureml-scaffolding-main 137 | ``` 138 | 139 | 2. Modify the [`pyproject.toml`](pyproject.toml) for your project. 140 | 1. Get familiar with [uv] if it is your first time working with it. 141 | 2. Modify the `name`, `version` and `description` to match your project's. 142 | 3. By default, we use `python 3.12`. To change it for the environment and all 143 | tools used: 144 | 1. Modify the `requires-python` key. 145 | 2. Modify the `pythonVersion` in the `[tool.pyright]`. 146 | 3. Modify the `target-version` in the `[tool.ruff]`. 147 | 4. Modify the `ARG PYTHON_VERSION` in the 148 | [`Dockerfile`](.devcontainer/Dockerfile). 149 | 3. Modify the [`.env`](.env) file to match your Azure. 150 | 1. Change the `AZUREML_WORKSPACE` value to the name of your workspace. 151 | 2. Change the `AZUREML_RESOURCE_GROUP` value to the name of the resource 152 | group your workspace belongs to. 153 | 3. Change the `AZUREML_TENANT_ID` value to the tenant ID of your Azure 154 | subscription. 155 | 4. Change the `name` key in the [`.devcontainer/devcontainer.json`](.devcontainer/devcontainer.json) file to 156 | match your project name. This is the name of the container that will be 157 | created. 158 | 5. Run the action `Dev Containers: Rebuild and Reopen in Container` in VSCode 159 | using the [Command Palette]. This will build the Dev Container for the first 160 | time. After it finishes, you are all set. 161 | 6. If it is the first time you use AzureML Scaffolding for a project of yours, 162 | the [Quickstart](#quickstart) is a good place to start. 163 | 7. If you do not wish to persist any of the example packages provided with the 164 | template, you can remove them by running the following: 165 | 166 | ```bash 167 | bin/pkg/rm example 168 | bin/pkg/rm example-reader-step 169 | bin/pkg/rm example-writer-step 170 | ``` 171 | 172 | 8. If you had existing code, move it to become one or more [packages]. You may 173 | need to [create a new package] and ensure the dependencies in its 174 | `pyproject.toml` match your existing requirements. 175 | 176 | [uv]: https://docs.astral.sh/uv/ 177 | [Command Palette]: 178 | https://code.visualstudio.com/docs/getstarted/userinterface#_command-palette 179 | [packages]: #packages 180 | [create a new package]: #creating-a-new-package 181 | 182 | ## Quickstart 183 | 184 | [Back to the Top](#table-of-contents) 185 | 186 | This quickstart will get you from zero to running your first ML experiment in 187 | Azure ML in about 10 minutes. By the end, you'll understand the core workflow of 188 | developing locally and executing remotely. 189 | 190 | ### Step 0: Prerequisites Check 191 | 192 | Before starting, ensure you have completed the [Installation](#installation) 193 | steps: 194 | 195 | - The repository is open in a VS Code Dev Container 196 | - Azure ML settings are configured in your `.env` file 197 | - The Dev Container has been built and is running 198 | 199 | ### Step 1: Verify Azure ML Connection 200 | 201 | Let's verify your Azure ML workspace connection is working: 202 | 203 | ```bash 204 | # Load environment variables and test connection 205 | . bin/env 206 | az login # If not already logged in 207 | az ml workspace show 208 | ``` 209 | 210 | This should display your workspace details. If you see an error, double-check 211 | your `.env` file settings. 212 | 213 | ### Step 2: Create Your First Package 214 | 215 | Let's create a simple training package that we can modify: 216 | 217 | ```bash 218 | # Load up environment variables 219 | . bin/env 220 | 221 | # Create a new package called "my-first-model" 222 | bin/pkg/new my-first-model 223 | ``` 224 | 225 | This creates a package with the standard structure in 226 | `packages/my-first-model/`. 227 | 228 | #### Step 3: Add Your ML Code 229 | 230 | Replace the contents of `packages/my-first-model/src/my_first_model/__main__.py` 231 | with your code. The contents in the file are illustrative of what you can do. 232 | This file is what gets executed down the line when you run the package. 233 | 234 | #### Step 4: Add Dependencies 235 | 236 | Add the required dependencies to your package. For example: 237 | 238 | ```bash 239 | uv add --package my-first-model scikit-learn joblib numpy 240 | ``` 241 | 242 | #### Step 5: Run Locally 243 | 244 | First, test your package locally to ensure it works: 245 | 246 | ```bash 247 | # Run in the full project environment (quick debugging) 248 | uv run python -m my_first_model 249 | # or equivalently 250 | uv run my_first_model 251 | # or use VSCode to run the __main__.py file directly 252 | 253 | # Run in isolated environment (recommended before cloud submission) 254 | bin/pkg/local my-first-model 255 | ``` 256 | 257 | Check the `runs/my-first-model/` folder to see the isolated execution artifacts. 258 | 259 | #### Step 6: Configure for Azure ML 260 | 261 | Edit `packages/my-first-model/aml-job.yaml` to set your compute target: 262 | 263 | ```yaml 264 | compute: azureml:YOUR-COMPUTE-NAME # Replace with your cluster name 265 | ``` 266 | 267 | #### Step 7: Run in Azure ML 268 | 269 | Now submit your experiment to Azure ML: 270 | 271 | ```bash 272 | # Basic submission 273 | bin/pkg/aml my-first-model 274 | 275 | # Or, as a tracked experiment with a description 276 | bin/pkg/aml --exp "Testing different sample sizes" my-first-model 277 | ``` 278 | 279 | The command will output a link to the Azure ML Studio where you can monitor your 280 | job's progress, view logs, and see the logged metrics. 281 | 282 | #### Understanding the Workflow 283 | 284 | What just happened: 285 | 286 | 1. **Local Development**: You wrote and tested code on your machine using the 287 | full development environment. 288 | 2. **Isolated Testing**: `bin/pkg/local` ran your code in an isolated 289 | environment with only its declared dependencies, catching any missing imports. 290 | 3. **Cloud Execution**: `bin/pkg/aml` packaged your code, created a Docker 291 | environment, and submitted it to Azure ML with full reproducibility. 292 | 293 | The same code ran in all three environments, demonstrating the "develop locally, 294 | run anywhere" principle. 295 | 296 | #### Next Steps 297 | 298 | Now that you've run your first experiment: 299 | 300 | - Modify the code and use `--exp` flag to track different experiments. 301 | - See what other commands are available in the CLI by running `bin/help`. 302 | - Try using Azure ML data assets (see [Data](#data) section). 303 | - Explore the generated `runs/` folder to understand the isolation mechanism. 304 | 305 | Continue reading the full documentation below for detailed explanations of all 306 | features. 307 | 308 | ## Usage 309 | 310 | [Back to the Top](#table-of-contents) 311 | 312 | The main interface of this project is the script-based CLI in the `bin` folder. 313 | These scripts abstract away the complexity in simple commands and ensure best 314 | practices are followed. Each available command of the CLI is defined by the 315 | filepath of the script. If you are interested in the details or wish to change 316 | some behavior, refer first to the [`bin/README.md](bin/README.md). Otherwise, 317 | that folder can be largely treated as a black box. The help should be enough to 318 | leverage the CLI. You can display it by running: 319 | 320 | ```text 321 | $ bin/help 322 | 323 | # Data 324 | 325 | bin/data/download Download data from Azure Blob Storage 326 | bin/data/find Find account, container and subdir for a named data asset 327 | bin/data/list List data in a Blob Storage Container 328 | bin/data/register Register a data asset in AzureML 329 | 330 | # Development 331 | 332 | bin/dev/sync Ensure dev env is in sync with the repo 333 | bin/dev/test Run pytest for the project with coverage 334 | 335 | # Environment 336 | 337 | bin/chkenv Checks if the specified environment variables are set 338 | bin/env Exports variables from an environment file 339 | 340 | # Linting 341 | 342 | bin/lint/md Lints Markdown files and validate their links 343 | bin/lint/py Format, line and type check all Python files in $PKGS_PATH 344 | bin/lint/shell Checks shell scripts for potential issues 345 | 346 | # Package 347 | 348 | bin/pkg/aml Execute a package in AzureML 349 | bin/pkg/local Execute package locally as it would in AzureML 350 | bin/pkg/new Initialize new package in the project 351 | bin/pkg/rm Remove a package from the workspace 352 | 353 | # Pipeline 354 | 355 | bin/pipe/aml Execute a pipeline in AzureML 356 | bin/pipe/local Execute pipeline locally as it would in AzureML 357 | bin/pipe/new Initialize new pipeline in the project 358 | ``` 359 | 360 | Most scripts have their own help, exposed through the `-h` or `--help` flags. 361 | You can use these to get more information about the specific command you are 362 | interested in. For example: 363 | 364 | ```text 365 | $ bin/pkg/new -h 366 | Initialize new package in the project 367 | 368 | Usage: bin/pkg/new PACKAGE 369 | 370 | Options: 371 | -h, --help Show this help message and exit 372 | 373 | Arguments: 374 | PACKAGE Name of the new package. 375 | 376 | The script will create a new package directory in the "$PKGS_PATH" directory 377 | using the '.package_template' as a template. It will also add the new package 378 | to the uv workspace and install it to the local environment. 379 | ``` 380 | 381 | These scripts are created to provide a good developer experience. To that end, 382 | they work in harmony with many other artifacts in the repo. You are encouraged 383 | to modify them to suit your needs, but beware that most decisions have been 384 | thought through and changing them may break some behavior: 385 | 386 | - The .`devcontainer` folder implements the definition for a devcontainer. It 387 | uses a [multi-stage] [`Dockerfile`] that serves for both full repository 388 | devcontainer and for packages environment. The [`devcontainer.json`] also 389 | installs useful VSCode extensions and necessary [devcontainer features]. 390 | - The `.vscode` folder contains the configuration for VSCode. It includes some 391 | configuration to make sure VSCode leverages the tools correctly. 392 | 393 | Finally, many of these scripts rely on the configuration through environment 394 | variables. These are defined in the [`.env`](.env) file. In many cases, if you 395 | are missing them, the script will fail with a clear error message. To make sure 396 | these variables are set in your shell: 397 | 398 | ```bash 399 | . bin/env 400 | ``` 401 | 402 | > **Tip**: The above command will set the environment variables from [`.env`]. 403 | > If you wish, that command also sets the variables in `.env.local` with higher 404 | > precedence if it exists. This second file is ignored by git and useful if 405 | > different developers need different values for the same variables. For more 406 | > details, run `bin/env -h`. 407 | 408 | [multi-stage]: https://docs.docker.com/build/building/multi-stage/ 409 | [`Dockerfile`]: .devcontainer/Dockerfile 410 | [`devcontainer.json`]: .devcontainer/devcontainer.json 411 | [devcontainer features]: https://containers.dev/features 412 | 413 | ### Data 414 | 415 | [Back to the Top](#table-of-contents) 416 | 417 | We recommend using [AzureML Data Assets] to manage data. This makes it easy to 418 | share data across people and machines and ensures traceability and lineage. 419 | 420 | [AzureML Data Assets]: 421 | https://learn.microsoft.com/en-us/azure/machine-learning/how-to-create-data-assets?view=azureml-api-2&tabs=cli 422 | 423 | #### Registering data 424 | 425 | [Back to the Top](#table-of-contents) 426 | 427 | The first step is to register the data in AzureML. In general, this means 428 | uploading the data and labeling it with a name and a version. The [AzureML Data 429 | Assets] page gives a good overview. In this project, we can use the following 430 | (which is a thin wrapper around the AzureML CLI) to register the data: 431 | 432 | ```bash 433 | bin/data/register 434 | ``` 435 | 436 | The `` is the path to the YAML file that defines the data asset. 437 | The minimal example is: 438 | 439 | ```yaml 440 | $schema: https://azuremlschemas.azureedge.net/latest/data.schema.json 441 | 442 | type: uri_folder 443 | name: 444 | version: 445 | description: 446 | path: ./relative/path/to/data 447 | ``` 448 | 449 | Once data is registered, it can be referenced in any AzureML YAML as 450 | `azureml::`. This is the recommended way to 451 | reference data in AzureML. Data then is mounted or downloaded to the compute 452 | target when the job is executed. This ensures clarity and traceability of the 453 | data used in each job. 454 | 455 | #### Downloading data 456 | 457 | [Back to the Top](#table-of-contents) 458 | 459 | Usually one person will register the data but many may wish to use it. Or even 460 | the same person in a different machine. For this, it is convenient to be able to 461 | download a data asset. This can be done using: 462 | 463 | ```bash 464 | bin/data/find NAME VERSION | xargs bin/data/download 465 | ``` 466 | 467 | The data will be downloaded in the `data` folder, which is ignored by git and is 468 | the recommended place for it. Downloading a data asset chains two commands 469 | because `bin/data/download` may be leveraged alone to download any data in 470 | Azure Blob Storage, registered or not. 471 | 472 | ### Packages 473 | 474 | [Back to the Top](#table-of-contents) 475 | 476 | A package is a self-contained unit of code that can be executed in isolation 477 | from the rest of the codebase. In this project, packages are Python packages 478 | defined in the `packages` folder. They are the base unit of execution. We 479 | leverage the [uv workspaces] feature to provide both a good developer experience 480 | through a master project environment and the ability to isolate runs to their 481 | minimal dependencies. The minimal file structure for a package is: 482 | 483 | ```text 484 | / 485 | ├── aml-job.yaml 486 | ├── environment/ 487 | │ └── Dockerfile 488 | ├── pyproject.toml 489 | ├── README.md 490 | ├── src/ 491 | │ └── 492 | │ ├── __init__.py 493 | │ └── __main__.py 494 | └── tests/ 495 | ``` 496 | 497 | It is worth explaining a few things here: 498 | 499 | - The top package name has dashes. The inner package name, where the code lives, 500 | has underscores. This is standard practice in Python and what [uv] does too. 501 | - **`__main__.py`** - This is the expected entrypoint of the package. When 502 | leveraging the `bin` scripts for local execution, this is the executed file. 503 | - **`pyproject.toml`** - The dependencies of the package should be defined here. 504 | This is what is used to build an isolated environment for execution. 505 | - **`environment/`** - This is the environment context. It should contain a 506 | `Dockerfile` that accepts a `requirements.txt` and builds an environment from 507 | it. The `requirements.txt` is generated from the dependencies defined in the 508 | `pyproject.toml` file at run time. We need a separate `environment/` folder 509 | because AzureML can cache the environment built from it if nothing changes. 510 | Having it at top level would mean a rebuild for every package change. 511 | - **`aml-job.yaml`** - This is the AzureML job specification for the 512 | package. It defines the compute target, the environment to use, the code to 513 | upload and the command to run. This is what is used to run the package in 514 | AzureML. This job can be of two types: 515 | - **[Command job]** - This is the simplest job type. It runs a command in a 516 | container. This is the most common type of job and the one we use for most 517 | of our experiments. 518 | - **[Pipeline job]** - This is a more complex job type that allows chaining 519 | multiple commands. The difference with a [Pipeline] as defined in this 520 | project, is that defining a pipeline job within a package means all steps 521 | will have all the same code and share the environment. Thus, any change in 522 | the package will eliminate the cache. The main benefit of this pattern is to 523 | avoid defining multiple packages for steps that largely use the same code. 524 | For instance, a training and inference step. 525 | 526 | An example package is provided in [`.package-template`]. It is useful as a 527 | reference and used by `bin/pkg/new` to create a new package. Most files there 528 | serve as live documentation. It includes examples on how to log metrics and 529 | tags, how to define the YAML file, how to import and how to define the 530 | environment. 531 | 532 | [uv workspaces]: https://docs.astral.sh/uv/concepts/projects/workspaces/ 533 | [Command job]: 534 | https://learn.microsoft.com/en-us/azure/machine-learning/reference-yaml-job-command?view=azureml-api-2 535 | [Pipeline job]: 536 | https://learn.microsoft.com/en-us/azure/machine-learning/reference-yaml-job-pipeline?view=azureml-api-2 537 | [`.package-template`]: .package-template 538 | 539 | #### Shared package 540 | 541 | [Back to the Top](#table-of-contents) 542 | 543 | This project supports a special package called `shared`. This package is meant 544 | to contain code that needs to be shared across multiple packages. At execution 545 | time, it is bundled with the package so it is available for execution. Points to 546 | have in mind: 547 | 548 | - The `pyproject.toml` of the `shared` package **should not contain 549 | dependencies**. It is the responsibility of the package to install what it 550 | needs. 551 | - Due to the above point, not all files in `shared` can be imported from all 552 | packages without errors. This is okay as long as the structure and imports are 553 | well thought. 554 | 555 | #### Creating a new package 556 | 557 | [Back to the Top](#table-of-contents) 558 | 559 | While you can create a package manually, we recommend using: 560 | 561 | ```bash 562 | bin/pkg/new 563 | ``` 564 | 565 | This creates the package from the `.package_template`, which ensures the package 566 | is created with the correct structure and files. Additionally, the command 567 | renames things in some files to match your package name and makes sure the 568 | package is added to the `uv workspace` and, thus, the local environment. The 569 | `Dockerfile` provided by the command works as it should, but feel free to change 570 | it if you wish to do so. 571 | 572 | #### Adding dependencies to a package 573 | 574 | [Back to the Top](#table-of-contents) 575 | 576 | To add dependencies needed for a package, you should add them to the 577 | corresponding `pyproject.toml` file (and never to the master `pyproject.toml` of 578 | the project). You can do that in different ways: 579 | 580 | - **Manually** - You can add the dependencies manually to the `pyproject.toml` 581 | file in the `dependencies` key. 582 | - **Using uv** - You can use the [uv] CLI to add dependencies. Run `uv add 583 | --help` for details. A short example is: 584 | 585 | ```bash 586 | uv add --package # e.g. `ruff==0.5.0` 587 | ``` 588 | 589 | > **Tip**: We usually specify dependencies with fixed major and minor versions 590 | > to avoid surprises. For example, `numpy==2.2.*` instead of `numpy>=2.2.0`. 591 | 592 | #### Executing a package 593 | 594 | [Back to the Top](#table-of-contents) 595 | 596 | We are strong proponents of the **Minimum Viable Compute** and this project aims 597 | to support it. We start developing in our laptops, move to bigger Virtual 598 | Machines when the code we are developing requires it (e.g., to ensure GPUs are 599 | used properly) and only after we have validated that the experiment runs in a 600 | complete but lightweight setup (e.g., a training with a batch size of 2, 10 601 | total samples for 1 epoch) we submit the experiment to run in Azure ML. This 602 | helps us **shorten feedback loops and speed up development**. 603 | 604 | Thus, the first step is usually to run the package locally. The easiest way to 605 | execute any code locally is leveraging VSCode. The [Python extension] allows 606 | users to easily run or debug any script. For this, it is easiest to have scripts 607 | that do not require any arguments. 608 | 609 | The problem with the previous approach is that it uses the project environment. 610 | This environment contains the dependencies of all packages as well as some extra 611 | dev dependencies. Thus, it is possible to miss dependency leaks. To run a 612 | package in locally in isolation: 613 | 614 | ```bash 615 | bin/pkg/local 616 | ``` 617 | 618 | This command will: 619 | 620 | 1. Isolate the package in the `runs//` folder. This will 621 | include `shared` package too. The `` is a memorable name for the run 622 | prefixed with the date and time of the execution to make it easier to reason 623 | about the runs. 624 | 2. Export the package dependencies into a `requirements.txt`. This captures 625 | everything, but not more, that needs to be in the environment for the payload 626 | to run. These first two steps ensure we can always recover a result if we 627 | have the run directory. 628 | 3. Execute the `__main__.py` of the package. This file is expected to be the 629 | entrypoint of the package, following [Python's standard practice for exposing 630 | a package CLI]. 631 | 632 | > **Tip**: We have found that a good pattern to maximize dev experience is to 633 | > have `__main__.py` to accept command line arguments but default to debug 634 | > values if none passed. This way, we can quickly change things locally without 635 | > having a potentially long command, which tends to be cumbersome. An example of 636 | > this pattern can be found in the 637 | > [`package-template`](.package-template/src/package_template/__main__.py). That 638 | > script also show how to log tags and metrics to AzureML. 639 | 640 | Once the isolated run succeeds locally, we are probably ready to submit the same 641 | payload to AzureML. To do so: 642 | 643 | ```bash 644 | bin/pkg/aml 645 | ``` 646 | 647 | This command will: 648 | 649 | 1. Isolate the package in the exact same way as `bin/pkg/local`. 650 | 2. Submit the package for execution to AzureML. The specification of the job is 651 | defined in the `aml-job.yaml` file. This file is expected to be present 652 | in the package root folder. The command will use the isolated run folder as 653 | the context for the job submission. It is the responsibility of the user to 654 | point that file to `__main__.py` to make the payload equivalent to the one 655 | that runs locally. 656 | 657 | Executing a run for a package in this manner, ensures the following: 658 | 659 | - All files (but only the files) required for the execution are uploaded to 660 | AzureML and present in the job UI. 661 | - The environment is built using only the `environment/` context, which, in 662 | turn, uses the `requirements.txt` generated. The environment definition is 663 | also bundled in the code, and thus it is always reproducible. 664 | - Downloading the code snapshot in AzureML and submitting directly that folder 665 | to AzureML will produce the exact same results. 666 | - Runs are present in AzureML. If linked to data, it is clear what data was used 667 | for the run. Additionally, everyone with access to the workspace can see and 668 | inspect the runs. 669 | - If the package was created using `bin/pkg/new`, all runs for a package will be 670 | grouped together in AzureML under the experiment with the same name as the 671 | package (defined by the `experiment_name` key in the YAML). 672 | 673 | > **Tip**: Getting the `aml-job.yaml` right the first time can be a bit 674 | > fiddly. For that, we recommend using the one in the `.package-template` as a 675 | > reference and starting point. 676 | 677 | To bring the experimentation game to the next level, you can make any AzureML 678 | execution an experiment by adding the `--exp` or `-e` flag to the command: 679 | 680 | ```bash 681 | bin/pkg/aml --exp "Add cosine scheduler" 682 | ``` 683 | 684 | This uses some `git` shenanigans to create a commit with all changes since main 685 | in the `experiments` branch locally. Then, by triggering the AzureML job from 686 | this `git` state, the commit linked in the AzureML UI contains all these 687 | changes. If job submission succeeds, this commit is pushed to the remote 688 | repository after ensuring that branch is updated with latest `main`. This has 689 | the following benefits: 690 | 691 | - The `experiments` branch will contain all experiments and will be persisted 692 | beyond the branches that were used to create them. This is useful to avoid 693 | having to keep all branches around forever. Otherwise, if AzureML links to a 694 | commit of a branch that is deleted, the link becomes broken. 695 | - The commit message will contain the name of the experiment. This makes it 696 | easier to find experiments (provided that devs put effort in naming). 697 | - The commit contains all changes made since main, no matter how many commits 698 | are in your branch at the moment. This allows for good atomic commits when 699 | developing but a single clear view of what is being tested in an experiment. 700 | Without this, AzureML links to the latest commit, which is usually a subset of 701 | the differences from main. 702 | 703 | ### Pipelines 704 | 705 | [Back to the Top](#table-of-contents) 706 | 707 | While most projects will start with one or a few isolated packages, many may 708 | benefit from leveraging pipelines at later stages. A pipeline defines a sequence 709 | of steps to execute. It is defined by a [Pipeline job] YAML 710 | `.yaml` in the `pipelines` folder. If unfamiliar with AzureML 711 | pipelines, reading the [pipeline documentation] is encouraged. As a primer, a 712 | pipeline has the following properties (which would inform when you want to use 713 | them): 714 | 715 | - One step outputs can be connected to the inputs of another one. 716 | - Steps connected will run sequentially, but the rest can run in parallel if 717 | sufficient compute is available. 718 | - Steps are cached. If submitted with the same code and environment, they will 719 | not be re-executed. This is useful for long running steps that do not change 720 | often. 721 | - Each step of the pipeline must reference a package. To do so, packages meant 722 | to be used in pipelines should implement a [Component] YAML defined. 723 | - Component inputs are displayed in AzureML UI. This helps with traceability. 724 | - The name of the job steps should match the name of the package referenced 725 | (with dashes changed to underscores due to AzureML expectation). 726 | 727 | An example pipeline is provided in 728 | [`.pipeline-template/pipeline-template.yaml`]. It is useful as a reference and 729 | used by `bin/pipe/new` to create a new pipeline. The rest of the files in that 730 | directory serve to create the example packages used by the pipeline. They also 731 | contain an example [Component] YAML: [`aml-component.yaml`]. 732 | 733 | > **Tip**: A single package may implement multiple steps. This is useful when 734 | > the steps are closely related and share a lot of code. For instance, a 735 | > training and inference step. In this case, the package should implement one 736 | > `aml-component.yaml` file per step. In these cases, `__main__.py` is usually 737 | > relegated to local usage only and is the responsibility of the user to ensure 738 | > the payload is equivalent to the multiple steps. 739 | 740 | [pipeline documentation]: 741 | https://learn.microsoft.com/en-us/azure/machine-learning/how-to-create-component-pipelines-cli?view=azureml-api-2 742 | [Component]: 743 | https://learn.microsoft.com/en-us/azure/machine-learning/reference-yaml-component-command?view=azureml-api-2 744 | [`.pipeline-template/pipeline-template.yaml`]: 745 | .pipeline-template/pipeline-template.yaml 746 | [`aml-component.yaml`]: 747 | .pipeline-template/example-reader-step/aml-component.yaml 748 | 749 | #### Creating a new pipeline 750 | 751 | [Back to the Top](#table-of-contents) 752 | 753 | While you can create a pipeline manually, we recommend using: 754 | 755 | ```bash 756 | bin/pipe/new 757 | ``` 758 | 759 | This creates a pipeline from the `.pipeline-template`. This creates the 760 | `.yaml` file in the `pipelines` folder. By default, it will 761 | create two packages in the `packages` folder: `example-reader-step` and 762 | `example-writer-step`. These packages are used to demonstrate how to use the 763 | pipeline. This is recommended for the first times. Once familiar, you may choose 764 | to use the `--no-packages` flag to avoid adding them. 765 | 766 | #### Executing a pipeline 767 | 768 | [Back to the Top](#table-of-contents) 769 | 770 | Similarly to packages, we can run a pipeline locally or in AzureML. 771 | 772 | While we can use VSCode to run the pipeline locally, it may become cumbersome as 773 | each step would need to be run separately. To make things a bit easier, we can 774 | run: 775 | 776 | ```bash 777 | bin/pipe/local 778 | ``` 779 | 780 | This command will: 781 | 782 | 1. Parse the pipeline YAML file to identify the packages used. This expects the 783 | `component` key to be of the form `.//...`. 784 | 2. Isolate the packages in the `runs//` folder. In this 785 | case, at the top level of the folder there will only be the pipeline YAML. 786 | Each package is isolated in its own subfolder in the same way `bin/pkg` 787 | scripts do. 788 | 3. Execute each package `__main__.py` in the order they are defined in pipeline 789 | YAML. It is the responsibility of the user to ensure each of these files can 790 | be executed without arguments and that inputs and outputs are properly 791 | connected where needed. 792 | 793 | Once this succeeds, once more we can execute the pipeline in AzureML in a similar 794 | way: 795 | 796 | ```bash 797 | bin/pipe/aml 798 | ``` 799 | 800 | This command will: 801 | 802 | 1. Isolate the packages in the exact same way as `bin/pipe/local`. 803 | 2. Submit the pipeline for execution to AzureML. The specification of the job is 804 | the `.yaml` file. It is the responsibility of the user to 805 | ensure it points to `aml-component.yaml` files for each step and that these 806 | files specify a payload equivalent to the one that runs locally. 807 | 808 | This command also accepts the `--exp` or `-e` flag to create an experiment. 809 | 810 | ### Linting 811 | 812 | [Back to the Top](#table-of-contents) 813 | 814 | In this project, the main driver for linting is the [`pre-commit`] hooks, which 815 | are installed by default and defined in [`.pre-commit-config.yaml`]. These block 816 | commits unless the code passes all checks. By default, we have 3 hooks enabled 817 | that execute the following commands (which may be called manually too): 818 | 819 | - **[`bin/lint/py`]** - Checks all Python files in the `packages` folder. It 820 | runs the following tools: 821 | - **[ruff]** - This is a fast linter and formatter for Python. It is used to 822 | format the code and check for common issues. Configuration is found in the 823 | [`pyproject.toml`]. 824 | - **[pyright]** - This is a type checker for Python. By default, it is set to 825 | `basic` mode. This means it will check for type errors if you define type 826 | hints but will not do anything if you do not. Some teams prefer to set it to 827 | `strict` mode, in which case all code must be type hinted. Configuration is 828 | found in the [`pyproject.toml`]. 829 | 830 | - **[`bin/lint/md`]** - Checks all Markdown files in the project. It runs the 831 | following tools: 832 | - **[markdownlint-cli2]** - This is a linter for Markdown files. It is used to 833 | check for common issues in Markdown files. Configuration is found in the 834 | [`.markdownlint-cli2.jsonc`]. 835 | - **[markdown-link-check]** - This is a linter for Markdown links. It is used 836 | to check for broken links in Markdown files. Configuration is found in the 837 | [`.markdown-link.json`]. 838 | 839 | - **[`bin/lint/shell`]** - Checks shell files in the project. It runs the 840 | following tool: 841 | - **[shellcheck]** - This is a linter for shell scripts. It is used to check 842 | for common issues in shell scripts. This is a bit more complex and 843 | configuration is found in the caller script itself. 844 | 845 | > **Tip**: In most cases, the user may ignore linting until the hooks run. 846 | > However, sometimes it is useful to run some of it manually. VSCode will pick 847 | > up `pyright` automatically and show errors in the editor. For `ruff`, the 848 | > default devcontainer installs the extension and will show errors. 849 | > Additionally, the `ruff` extension offers format imports and format code 850 | > commands exposed in the command palette. For markdown, some errors are raised 851 | > in the editor. We install the rewrap extension. The command "Rewrap Comment" 852 | > in VSCode is useful to ensure the line length is respected. For `shellcheck`, 853 | > the extension is installed and will also show errors in the editor. 854 | 855 | [`pre-commit`]: https://pre-commit.com/ 856 | [ruff]: https://docs.astral.sh/ruff/ 857 | [pyright]: https://github.com/microsoft/pyright 858 | [markdownlint-cli2]: https://github.com/DavidAnson/markdownlint-cli2 859 | [markdown-link-check]: https://github.com/tcort/markdown-link-check 860 | [shellcheck]: https://www.shellcheck.net/ 861 | [`bin/lint/py`]: bin/lint/py 862 | [`bin/lint/md`]: bin/lint/md 863 | [`bin/lint/shell`]: bin/lint/shell 864 | [`pyproject.toml`]: pyproject.toml 865 | [`.pre-commit-config.yaml`]: .pre-commit-config.yaml 866 | [`.markdownlint-cli2.jsonc`]: .markdownlint-cli2.jsonc 867 | 868 | ### Testing 869 | 870 | [Back to the Top](#table-of-contents) 871 | 872 | We use and recommend [pytest] for testing. A little wrapper command 873 | `bin/dev/test` is provided to run the tests with coverage. 874 | -------------------------------------------------------------------------------- /bin/README.md: -------------------------------------------------------------------------------- 1 | # Script-based CLI 2 | 3 | As [mentioned in the project `README.md`], the main interface of this project is 4 | the script-based command-line interface (CLI) implemented in this folder. This 5 | document describes some details for developers interested in understanding the 6 | inner workings and/or modifying the CLI. 7 | 8 | - A command is an executable script without extension in this folder. To make 9 | a script executable, it needs a shebang line at the top and it needs to be made 10 | executable by `chmod +x script_name`. 11 | - Commands are organized into subfolders for logical grouping. 12 | - [`bin/help`] has the following properties: 13 | - The displayed help comes from the line starting with `#?` in the script. 14 | - Commands are grouped by the `[...]` right after the `#?` line. 15 | - Only executable scripts without extension and without a prefix `_` are 16 | displayed. The non-displayed scripts are considered private. 17 | - `bin/lib` is a special folder that contains scripts and libraries that are 18 | not intended to be executed directly. They are used by other scripts in the 19 | `bin` folder. There are two types of content: 20 | - `.sh` files are shell scripts that are sourced. By convention, they 21 | define only functions. All functions defined in such files are named 22 | `::`. This way, it is easier to identify the source of the 23 | function. For example, in `run.sh` we define the function `run::local`. 24 | - `_` files are executable scripts never meant to be executed directly 25 | by the user. They are used by other scripts. 26 | - Check existing scripts for the usual patterns such as argument parsing. 27 | - It is important that all public scripts (displayed in the help) expose a `-h` 28 | or `--help` option that displays the help. 29 | 30 | [mentioned in the project `README.md`]: ../README.md#usage 31 | [`bin/help`]: help 32 | -------------------------------------------------------------------------------- /bin/chkenv: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Environment] Checks if the specified environment variables are set 3 | 4 | . bin/lib/utils.sh 5 | if [ $# -eq 0 ]; then utils::invalid_usage "Usage: $0 VAR1 [VAR2 ...]"; fi 6 | 7 | error=0 8 | for var in "$@"; do 9 | if [ -z "${!var}" ]; then 10 | echo>&2 "Missing environment variables: $var" 11 | error=1 12 | fi 13 | done 14 | 15 | if [ $error -eq 1 ]; then 16 | echo>&2 "Have you forgotten to execute 'source bin/env'?" 17 | fi 18 | 19 | exit $error 20 | -------------------------------------------------------------------------------- /bin/data/download: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Data] Download data from Azure Blob Storage 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | usage() { 8 | cat<&2; fi 34 | 35 | account=$1 36 | container=$2 37 | pattern=$3 38 | 39 | bin/chkenv TENANT_ID 40 | 41 | AZCOPY_AUTO_LOGIN_TYPE=azcli AZCOPY_TENANT_ID="$TENANT_ID" azcopy copy \ 42 | "https://$account.blob.core.windows.net/$container" \ 43 | "data" \ 44 | --recursive --include-regex "$pattern" -------------------------------------------------------------------------------- /bin/data/find: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Data] Find account, container and subdir for a named data asset 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | usage() { 8 | cat< to stdout. 18 | is returned as an anchored regex pattern to be consumed by download. 19 | EOF 20 | } 21 | 22 | while :; do 23 | case $1 in 24 | -h|--help) usage; exit ;; 25 | *) break ;; 26 | esac 27 | shift 28 | done 29 | 30 | . bin/lib/utils.sh 31 | if [[ $# -ne 2 ]]; then utils::invalid_usage "Exactly 2 arguments expected, but $# supplied."; fi 32 | name=$1 33 | version=$2 34 | 35 | bin/chkenv AZUREML_WORKSPACE AZUREML_RESOURCE_GROUP 36 | 37 | dataset_path="$(az ml data show --name "$name" \ 38 | --version "$version" \ 39 | --workspace-name "$AZUREML_WORKSPACE" \ 40 | --resource-group "$AZUREML_RESOURCE_GROUP" \ 41 | --query "path" --output tsv)" 42 | 43 | # URI is of the form: .../datastores//paths/ 44 | datastore_path="${dataset_path#*datastores/}" # Remove everything before "datastores" 45 | datastore="${datastore_path%%/paths*}" # Remove everything after "paths" 46 | dataset_subdir="${datastore_path#*paths/}" # Keep everything after "paths" 47 | 48 | { 49 | read -r container 50 | read -r account 51 | } < <( az ml datastore show --name "$datastore" \ 52 | --workspace-name "$AZUREML_WORKSPACE" \ 53 | --resource-group "$AZUREML_RESOURCE_GROUP" \ 54 | --query "[container_name, account_name]" \ 55 | --output tsv 56 | ) 57 | 58 | # dataset_subdir as anchored regex pattern to be consumed by download 59 | echo "$account" "$container" "^$dataset_subdir" 60 | -------------------------------------------------------------------------------- /bin/data/list: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Data] List data in a Blob Storage Container 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | usage() { 8 | cat<&2 "Error: Invalid argument: $1" 54 | exit 1 55 | fi 56 | fi 57 | 58 | ENVFILE=${ENVFILE:-$__env_default_envfile} 59 | 60 | # Split the colon-separated list of files into an array 61 | IFS=':' read -r -a __env_files <<< "$ENVFILE" 62 | 63 | if [[ "$0" == "${BASH_SOURCE[0]}" ]]; then 64 | echo >&2 "Warning! Variables will not be defined or exported into your current shell unless this script is sourced as follows: source ${BASH_SOURCE[0]}" 65 | fi 66 | 67 | __env_debug() { 68 | [ -n "$ENVDEBUG" ] 69 | } 70 | 71 | __env_load() { 72 | local __env_file __env_line __env_var __env_val 73 | __env_file=$1 74 | while IFS= read -r __env_line; do 75 | if [[ "$__env_line" =~ ^[[:space:]]*# || "$__env_line" =~ ^[[:space:]]*$ ]]; then 76 | continue # Skip comments and blank lines 77 | fi 78 | # Split line by "=" into variable name and value 79 | IFS='=' read -r __env_var __env_val <<< "$__env_line" 80 | __env_var=${__env_var#"${__env_var%%[![:space:]]*}"} # Remove leading whitespace 81 | __env_var=${__env_var%"${__env_var##*[![:space:]]}"} # Remove trailing whitespace 82 | if [ ! -v "$__env_var" ]; then # Only export if variable is not already defined 83 | __env_val=$(envsubst <<< "$__env_val") # Perform variable substitution in value 84 | __env_debug && echo >&2 "$__env_file: exported: $__env_var=$__env_val" 85 | export "$__env_var=$__env_val" 86 | elif __env_debug; then 87 | echo >&2 "$__env_file: skipped: $__env_var (already defined)" 88 | fi 89 | done < "$__env_file" 90 | } 91 | 92 | for __env_file in "${__env_files[@]}"; do 93 | if [ -f "$__env_file" ]; then 94 | __env_load "$__env_file" 95 | elif __env_debug; then 96 | echo >&2 "Skipping non-existent file: $__env_file" 97 | fi 98 | done 99 | 100 | unset "${!__env_@}" # Clean-up our variables for when sourced 101 | -------------------------------------------------------------------------------- /bin/help: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | # ? Show this help 3 | 4 | import os 5 | import re 6 | from collections import OrderedDict 7 | from collections.abc import Iterable, Iterator 8 | from dataclasses import dataclass 9 | from enum import Enum 10 | from itertools import groupby 11 | from operator import itemgetter 12 | from os import fspath 13 | from pathlib import Path 14 | from typing import Final 15 | 16 | # The help pattern (in pseudo-BNF below) is used to extract the description of a 17 | # command: 18 | # 19 | # HELP ::= "#" (" ") "?" ( "[" GROUP "]" ) DESCRIPTION 20 | # GROUP ::= any character except "]" 21 | # DESCRIPTION ::= any character 22 | # 23 | # Example: 24 | # 25 | # #? [Utility] This is a super duper useful command 26 | 27 | default_help_line_pattern: Final = re.compile(r"^# ?\? *(?:\[(?P[^\]]+)\] *)?(?P.+)") 28 | 29 | DEFAULT_GROUP = "Misc" 30 | 31 | 32 | def scan_commands( 33 | files: Iterable[Path], 34 | *, 35 | help_line_pattern: re.Pattern[str] | None = None, 36 | group_index: str | int = 1, 37 | help_index: str | int = 2, 38 | default_group: str = DEFAULT_GROUP, 39 | ) -> Iterator[tuple[Path, str, str]]: 40 | """ 41 | Scan the files, yielding the file, group, and help text. 42 | """ 43 | help_line_pattern = help_line_pattern or default_help_line_pattern 44 | for file in files: 45 | with file.open() as lines: 46 | match = next((m for line in lines if (m := help_line_pattern.match(line))), None) 47 | group, help_ = ( 48 | ((match.group(group_index) or default_group).strip(), match.group(help_index).strip()) 49 | if match 50 | else (default_group, "") 51 | ) 52 | yield (file, group, help_) 53 | 54 | 55 | def should_display_help(filepath: Path) -> bool: 56 | return ( 57 | filepath.is_file() 58 | and os.access(filepath, os.X_OK) 59 | and not filepath.suffix 60 | and not filepath.name.startswith("_") 61 | ) 62 | 63 | 64 | @dataclass(frozen=True) 65 | class Command: 66 | path: Path 67 | path_str: str 68 | group: str 69 | group_key: str 70 | help: str 71 | 72 | 73 | def main() -> None: 74 | """ 75 | Print the description of all the commands (executable scripts) in the same 76 | directory as this script, including sub-directories. Commands are grouped. 77 | 78 | The description of a command is the first line of the file that starts with 79 | the `help_pattern`. A typical help line looks like this: 80 | 81 | "# ?" ( "[" GROUP "]" ) DESCRIPTION 82 | 83 | The GROUP is optional and is used to group commands together. If no GROUP is 84 | provided, the command is grouped under a default group. 85 | """ 86 | scan_root_path = Path(__file__).parent 87 | 88 | commands = [ 89 | Command( 90 | path=file, 91 | path_str=fspath(file.relative_to(scan_root_path.parent)), 92 | group=group, 93 | group_key=group.lower(), # normalized for grouping/sorting 94 | help=help_, 95 | ) 96 | for file, group, help_ in scan_commands( 97 | path 98 | for path in scan_root_path.rglob("*") 99 | if should_display_help(path) 100 | ) 101 | if fspath(file) != __file__ # exclude this file 102 | ] 103 | 104 | max_file_name_length = max(len(c.path_str) for c in commands) 105 | 106 | commands_by_group = OrderedDict( 107 | (group, list(m[-1] for m in members)) 108 | for group, members in groupby( 109 | # sort by group, then by path 110 | sorted(((c.group_key, c.path_str, c) for c in commands), key=itemgetter(0, 1)), 111 | key=itemgetter(0), 112 | ) 113 | ) 114 | 115 | for i, (_, commands) in enumerate(commands_by_group.items()): 116 | if i > 0: 117 | cprint() 118 | 119 | if len(commands_by_group) > 1: 120 | cprint(f"# {commands[0].group}", color=Color.BLUE) 121 | cprint() 122 | 123 | for command in commands: 124 | cprint(command.path_str, color=Color.CYAN, end="") 125 | cprint(f"{' ' * (max_file_name_length + 2 - len(command.path_str))} {command.help}") 126 | 127 | 128 | class Color(Enum): 129 | CYAN = "\033[96m" 130 | BLUE = "\033[94m" 131 | DEFAULT = "\033[0m" 132 | 133 | 134 | def cprint( 135 | message: str = "", 136 | color: Color = Color.DEFAULT, 137 | *, 138 | end: str | None = "\n", 139 | ) -> None: 140 | print(color.value + message if os.isatty(1) else message, end=end) 141 | 142 | 143 | if __name__ == "__main__": 144 | main() 145 | -------------------------------------------------------------------------------- /bin/lib/_aml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? Base script to execute a run in AzureML 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | usage() { 8 | cat< /dev/null 78 | else 79 | bin/pkg/_iso "$artifact" "$run_dir" > /dev/null 80 | fi 81 | 82 | aml_run() { 83 | az ml job create -f "$run_dir/$file" \ 84 | --resource-group "$AZUREML_RESOURCE_GROUP" \ 85 | --workspace-name "$AZUREML_WORKSPACE" \ 86 | --set name="$run_name" \ 87 | --set description="$exp" \ 88 | "${job_xargs[@]}" 89 | } 90 | if [[ -z "$exp" ]]; then 91 | # If no experiment name is provided, run the job from current branch 92 | aml_run 93 | else 94 | # If an experiment name is provided, track changes and run from experiments branch 95 | bin/chkenv "EXPERIMENTS_BRANCH" 96 | 97 | commit_id=$(bin/lib/_prep-exp --exp-ref "$EXPERIMENTS_BRANCH" "$exp") 98 | curr_branch=$(git rev-parse --abbrev-ref HEAD) 99 | git checkout "$EXPERIMENTS_BRANCH" 100 | 101 | if aml_run; then 102 | # Push commit to experiment branch only if job submission succeeded 103 | git push "origin" "$commit_id:$EXPERIMENTS_BRANCH" 104 | fi 105 | 106 | # Restore working directory to the original branch. 107 | git checkout "$curr_branch" 108 | fi 109 | 110 | -------------------------------------------------------------------------------- /bin/lib/_prep-exp: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? Prepare git state for proper experiment tracing 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | usage() { 8 | cat<&2 "Working directory is dirty. Retry after committing or stashing changes." && 78 | exit 1 79 | fi 80 | 81 | # Resolve the source into a symbolic/branch name, in case something like "HEAD" 82 | # was specified. 83 | 84 | src_ref=$(git rev-parse --abbrev-ref --symbolic-full-name "$src_ref") 85 | 86 | # Make sure that all remote refs are up to date. 87 | 88 | git fetch "$remote" 89 | 90 | # Find out the upstream branch of the main branch and... 91 | 92 | up_main_ref=$(git rev-parse --abbrev-ref --symbolic-full-name "$main_ref@{upstream}") 93 | up_main_id=$(git rev-parse "$up_main_ref") 94 | merge_base_id=$(git merge-base "$up_main_ref" "$src_ref") 95 | 96 | # ...check if the current branch is up to date with it: 97 | 98 | if [[ "$merge_base_id" != "$up_main_id" ]]; then 99 | 100 | # It's not up to date, so unless auto-merge is requested, exit with an 101 | # error. 102 | echo >&2 "The branch is not up to date with: $up_main_ref" 103 | echo >&2 "Retry after running: git merge $up_main_ref" 104 | exit 1 105 | 106 | fi 107 | 108 | # Get the hash of the main upstream tree. 109 | 110 | up_main_tree_id=$(git log -1 --pretty=%T "$up_main_ref") 111 | 112 | # Get the hashes of the experiments commit (head) and tree delimited by colon 113 | # (:) then split and parse each one into separate variables. 114 | 115 | IFS=: read -r exp_head_id exp_tree_id < <( 116 | git log -1 --pretty="%H:%T" "$remote_exp_ref" 117 | ) 118 | 119 | # Search the trees of the experiments branch history to see if the tree of 120 | # source reference head is there. If it is, bail out because there is nothing 121 | # new to do. 122 | src_tree_id=$(git log -1 --pretty=%T "$src_ref") 123 | if grep -q -F "$src_tree_id" < <(git log --pretty=%T "$remote_exp_ref"); then 124 | echo >&2 "Experiment has already been pushed; nothing new to do." 125 | exit 0 126 | fi 127 | 128 | # If the experiments tree is not in sync with the main upstream tree, then 129 | # create a new commit that restores the main upstream branch tree on to the 130 | # experiments branch. 131 | 132 | if [[ "$up_main_tree_id" != "$exp_tree_id" ]]; then 133 | exp_head_id=$( 134 | git commit-tree \ 135 | -p "$remote_exp_ref" \ 136 | -m "restore main ($(git log -1 --pretty=%h "$up_main_id"))" \ 137 | "$up_main_tree_id" 138 | ) 139 | else 140 | echo >&2 "Trees of \"$up_main_ref\" and \"$remote_exp_ref\" are in sync; skipping sync." 141 | fi 142 | 143 | # Commit the tree of the supplied commit hash on to the local experiments 144 | # branch. 145 | 146 | commit_id=$(git commit-tree -p "$exp_head_id" -m "$message" "$src_tree_id") 147 | git update-ref "refs/heads/$exp_ref" "$commit_id" 148 | 149 | echo "$commit_id" 150 | -------------------------------------------------------------------------------- /bin/lib/run.sh: -------------------------------------------------------------------------------- 1 | # Library file for functions related to runs 2 | 3 | if [ "${BASH_SOURCE[0]}" = "$0" ]; then 4 | echo >&2 "This script must be sourced, not executed." 5 | exit 1 6 | fi 7 | 8 | run::prepare() { 9 | if [ $# -eq 0 ]; then 10 | echo>&2 "GROUP not provided." 11 | echo>&2 "Usage: $0 GROUP" 12 | exit 1 13 | fi 14 | 15 | bin/chkenv "RUNS_PATH" 16 | local run_group=$1 # usually package or pipeline name 17 | 18 | local run_name 19 | run_name=$(run::name) 20 | # add timestamp locally so runs are sorted chronologically 21 | local local_run_name 22 | local_run_name="$(date +%Y%m%d_%H%M%S)_$run_name" 23 | local run_dir="$RUNS_PATH/$run_group/$local_run_name" 24 | echo "$run_name" "$run_dir" 25 | } 26 | 27 | run::local() { 28 | if [ $# -eq 0 ]; then 29 | echo>&2 "PACKAGE not provided." 30 | echo>&2 "Usage: ${FUNCNAME[0]} PACKAGE" 31 | exit 1 32 | fi 33 | 34 | local package=$1 35 | uv run \ 36 | --with-requirements "../environment/requirements.txt" \ 37 | --isolated \ 38 | --no-project \ 39 | -m "${package//-/_}" 40 | } 41 | 42 | 43 | # Create a random and memorable name for a run 44 | run::name() { 45 | # Adjectives and nouns copied from Moby project: 46 | # https://github.com/moby/moby/blob/master/pkg/namesgenerator/names-generator.go 47 | local adjectives=( 48 | "admiring" "adoring" "affectionate" "agitated" "amazing" 49 | "angry" "awesome" "beautiful" "blissful" "bold" "boring" 50 | "brave" "busy" "charming" "clever" "compassionate" "competent" 51 | "condescending" "confident" "cool" "cranky" "crazy" "dazzling" 52 | "determined" "distracted" "dreamy" "eager" "ecstatic" "elastic" 53 | "elated" "elegant" "eloquent" "epic" "exciting" "fervent" 54 | "festive" "flamboyant" "focused" "friendly" "frosty" "funny" 55 | "gallant" "gifted" "goofy" "gracious" "great" "happy" 56 | "hardcore" "heuristic" "hopeful" "hungry" "infallible" "inspiring" 57 | "intelligent" "interesting" "jolly" "jovial" "keen" "kind" 58 | "laughing" "loving" "lucid" "magical" "modest" "musing" 59 | "mystifying" "naughty" "nervous" "nice" "nifty" "nostalgic" 60 | "objective" "optimistic" "peaceful" "pedantic" "pensive" "practical" 61 | "priceless" "quirky" "quizzical" "recursing" "relaxed" "reverent" 62 | "romantic" "sad" "serene" "sharp" "silly" "sleepy" 63 | "stoic" "strange" "stupefied" "suspicious" "sweet" "tender" 64 | "thirsty" "trusting" "unruffled" "upbeat" "vibrant" "vigilant" 65 | "vigorous" "wizardly" "wonderful" "xenodochial" "youthful" "zealous" 66 | "zen" 67 | ) 68 | 69 | local nouns=( 70 | "archimedes" "ardinghelli" "aryabhata" "austin" "babbage" 71 | "banach" "banzai" "bardeen" "bartik" "bassi" 72 | "beaver" "bell" "benz" "bhabha" "bhaskara" 73 | "black" "blackburn" "blackwell" "bohr" "booth" 74 | "borg" "bose" "bouman" "boyd" "brahmagupta" 75 | "brattain" "brown" "buck" "burnell" "cannon" 76 | "carson" "cartwright" "carver" "cerf" "chandrasekhar" 77 | "chaplygin" "chatelet" "chatterjee" "chaum" "chebyshev" 78 | "clarke" "cohen" "colden" "cori" "cray" 79 | "curie" "curran" "darwin" "davinci" "dewdney" 80 | "dhawan" "diffie" "dijkstra" "dirac" "driscoll" 81 | "dubinsky" "easley" "edison" "einstein" "elbakyan" 82 | "elgamal" "elion" "ellis" "engelbart" "euclid" 83 | "euler" "faraday" "feistel" "fermat" "fermi" 84 | "feynman" "franklin" "gagarin" "galileo" "galois" 85 | "ganguly" "gates" "gauss" "germain" "goldberg" 86 | "goldstine" "goldwasser" "golick" "goodall" "gould" 87 | "greider" "grothendieck" "haibt" "hamilton" "haslett" 88 | "hawking" "heisenberg" "hellman" "hermann" "herschel" 89 | "hertz" "heyrovsky" "hodgkin" "hofstadter" "hoover" 90 | "hopper" "hugle" "hypatia" "ishizaka" "jackson" 91 | "jang" "jemison" "jennings" "jepsen" "johnson" 92 | "joliot" "jones" "kalam" "kapitsa" "kare" 93 | "keldysh" "keller" "kepler" "khayyam" "khorana" 94 | "kilby" "kirch" "knuth" "kowalevski" "lalande" 95 | "lamarr" "lamport" "leakey" "leavitt" "lederberg" 96 | "lehmann" "lewin" "lichterman" "liskov" "lovelace" 97 | "lumiere" "mahavira" "margulis" "matsumoto" "maxwell" 98 | "mayer" "mccarthy" "mcclintock" "mclaren" "mclean" 99 | "mcnulty" "meitner" "mendel" "mendeleev" "meninsky" 100 | "merkle" "mestorf" "mirzakhani" "montalcini" "moore" 101 | "morse" "moser" "murdock" "napier" "nash" 102 | "neumann" "newton" "nightingale" "nobel" "noether" 103 | "northcutt" "noyce" "panini" "pare" "pascal" 104 | "pasteur" "payne" "perlman" "pike" "poincare" 105 | "poitras" "proskuriakova" "ptolemy" "raman" "ramanujan" 106 | "rhodes" "ride" "ritchie" "robinson" "roentgen" 107 | "rosalind" "rubin" "saha" "sammet" "sanderson" 108 | "satoshi" "shamir" "shannon" "shaw" "shirley" 109 | "shockley" "shtern" "sinoussi" "snyder" "solomon" 110 | "spence" "stonebraker" "sutherland" "swanson" "swartz" 111 | "swirles" "taussig" "tesla" "tharp" "thompson" 112 | "torvalds" "tu" "turing" "varahamihira" "vaughan" 113 | "villani" "visvesvaraya" "volhard" "wescoff" "wilbur" 114 | "wiles" "williams" "williamson" "wilson" "wing" 115 | "wozniak" "wright" "wu" "yalow" "yonath" 116 | "zhukovsky" 117 | ) 118 | 119 | # Pick a random adjective and noun 120 | local adjective=${adjectives[$RANDOM % ${#adjectives[@]}]} 121 | local noun=${nouns[$RANDOM % ${#nouns[@]}]} 122 | 123 | # Generate 8 random letters 124 | local random_letters 125 | random_letters=$(tr -dc 'a-z0-9' < /dev/urandom | fold -w 6 | head -n 1) 126 | 127 | # Concatenate the parts 128 | local name="${adjective}_${noun}_${random_letters}" 129 | 130 | # Output the result 131 | echo "$name" 132 | } 133 | -------------------------------------------------------------------------------- /bin/lib/utils.sh: -------------------------------------------------------------------------------- 1 | # Library file for general utility functions 2 | 3 | if [ "${BASH_SOURCE[0]}" = "$0" ]; then 4 | echo >&2 "This script must be sourced, not executed." 5 | exit 1 6 | fi 7 | 8 | 9 | utils::invalid_usage() { 10 | echo >&2 "$1" 11 | usage >&2 # Must be defined in script that sources this one 12 | exit 1 13 | } -------------------------------------------------------------------------------- /bin/lint/md: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Linting] Lints Markdown files and validate their links 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | # Usage documentation 8 | usage() { 9 | cat<&2 'No executable and tracked shell scripts found.' 37 | exit 38 | fi 39 | "$0" check "${MAPFILE[@]}" 40 | } 41 | 42 | check_git() { 43 | if ! git ls-files '?' --format=test 1>/dev/null 2>&1; then 44 | if ! git rev-parse --is-inside-work-tree &>/dev/null; then 45 | echo >&2 'Not in a Git repository.' 46 | else 47 | echo >&2 'You need a version of Git whose "ls-files" sub-command supports the "--format" option.' 48 | fi 49 | exit 1 50 | fi 51 | } 52 | 53 | case "$1" in 54 | "") 55 | check_git 56 | check_all 57 | exit 58 | ;; 59 | ls-tracked) 60 | check_git 61 | git ls-files --format="%(objectmode):%(path)" | 62 | grep -E '^100755:' | # Only executable files 63 | while IFS=: read -r mode path; do 64 | : "$mode" # Unused 65 | # Emit path if a shebang (#!) is found on the first line followed by 66 | # "bash": 67 | if head -1 "$path" | grep -q -E '^#!.+\bbash\b'; then 68 | echo "$path" 69 | fi 70 | done 71 | ;; 72 | check) 73 | shift 74 | if [ $# -eq 0 ]; then 75 | check_all 76 | else 77 | shellcheck -x -s bash "$@" 78 | fi 79 | ;; 80 | help) 81 | usage 82 | ;; 83 | *) 84 | echo >&2 "Unknown command: $1" 85 | usage >&2 86 | exit 1 87 | ;; 88 | esac 89 | -------------------------------------------------------------------------------- /bin/pipe/_iso: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Pipeline] Isolate files for pipeline run in a separate directory 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | usage() { 8 | cat< 30 | ├── .yaml 31 | ├── / 32 | │ └── ... 33 | └── / 34 | └── ... 35 | EOF 36 | } 37 | 38 | 39 | while :; do 40 | case $1 in 41 | -h|--help) usage; exit ;; 42 | *) break ;; 43 | esac 44 | shift 45 | done 46 | 47 | . bin/lib/utils.sh 48 | if [ $# -ne 2 ]; then utils::invalid_usage "Both PIPELINE and RUN_DIR required."; fi 49 | 50 | bin/chkenv "PKGS_PATH" "PIPES_PATH" 51 | pipeline=$1 52 | run_dir=$2 53 | 54 | pipeline_path="./$PIPES_PATH/$pipeline.yaml" 55 | if [[ ! -f "$pipeline_path" ]]; then 56 | echo>&2 "Pipeline file '$pipeline_path' does not exist." 57 | exit 1 58 | fi 59 | 60 | output=$(uv run python - < /dev/null 51 | run::local "$package" 52 | popd > /dev/null 53 | done <<< "$packages" 54 | -------------------------------------------------------------------------------- /bin/pipe/new: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Pipeline] Initialize new pipeline in the project 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | usage() { 8 | cat< 27 | ├── .amlignore 28 | ├── aml-job.yaml 29 | ├── shared/ 30 | ├── tests/ 31 | ├── environment/ 32 | │ ├── Dockerfile 33 | │ └── requirements.txt 34 | └── 35 | ├── __main__.py 36 | └── ... 37 | 38 | The requirements.txt will contain only the dependencies specified for the 39 | package. 40 | EOF 41 | } 42 | 43 | 44 | while :; do 45 | case $1 in 46 | -h|--help) usage; exit ;; 47 | *) break ;; 48 | esac 49 | shift 50 | done 51 | 52 | . bin/lib/utils.sh 53 | if [ $# -ne 2 ]; then utils::invalid_usage "Both PACKAGE and RUN_NAME required."; fi 54 | package=$1 55 | run_dir=$2 56 | 57 | bin/chkenv "PKGS_PATH" 58 | if [[ ! -d "$PKGS_PATH/$package" ]]; then 59 | echo>&2 "Package directory '$PKGS_PATH/$package' does not exist." 60 | exit 1 61 | fi 62 | 63 | # ensure run_dir exists 64 | mkdir -p "$run_dir" 65 | 66 | # copy package code 67 | cp -r "$PKGS_PATH/$package/." "$run_dir" 68 | # if shared dir is present, copy it to make it available too 69 | if [[ -d "$PKGS_PATH/shared" ]]; then 70 | cp -r "$PKGS_PATH/shared/src/shared" "$run_dir/src" 71 | mkdir -p "$run_dir/tests/shared" 72 | cp -r "$PKGS_PATH/shared/tests/." "$run_dir/tests/shared" 73 | fi 74 | 75 | # copy artifacts to build environment 76 | docker_context="$run_dir/environment" 77 | mkdir -p "$docker_context" 78 | uv export --quiet \ 79 | --output-file "$docker_context/requirements.txt" \ 80 | --package "$package" \ 81 | --no-hashes \ 82 | --no-emit-workspace 83 | -------------------------------------------------------------------------------- /bin/pkg/aml: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Package] Execute a package in AzureML 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | 8 | usage() { 9 | cat< /dev/null 44 | run::local "$package" 45 | popd > /dev/null 46 | -------------------------------------------------------------------------------- /bin/pkg/new: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | #? [Package] Initialize new package in the project 3 | 4 | set -eo pipefail 5 | cd "$(dirname "$0")/../.." 6 | 7 | usage() { 8 | cat<&2 "Package directory '$PKGS_PATH/$package' does not exist." 39 | exit 1 40 | fi 41 | 42 | rm -rf "${PKGS_PATH:?}/$package" 43 | uv remove "$package" -------------------------------------------------------------------------------- /data/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /notebooks/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /packages/example-reader-step/.amlignore: -------------------------------------------------------------------------------- 1 | # To prevent unnecessary files from being included in 2 | # the AzureML code snapshot, make an ignore file (.amlignore). 3 | # Place this file in the Snapshot directory and add the 4 | # filenames to ignore in it. The .amlignore file uses 5 | # the same syntax and patterns as the .gitignore file. 6 | # If no .amlignore file is present, the .gitignore file 7 | # is used to determine which files to ignore. 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # Unit test cache 15 | .pytest_cache/ 16 | 17 | # macOS stuff 18 | **/.DS_Store -------------------------------------------------------------------------------- /packages/example-reader-step/README.md: -------------------------------------------------------------------------------- 1 | # example_reader_step 2 | -------------------------------------------------------------------------------- /packages/example-reader-step/aml-component.yaml: -------------------------------------------------------------------------------- 1 | $schema: https://azuremlschemas.azureedge.net/latest/commandComponent.schema.json 2 | type: command 3 | 4 | name: example_reader_step 5 | display_name: Example Reader Step 6 | 7 | # What to run (always cd into src for imports to work) 8 | command: >- 9 | cd src && 10 | python -m example_reader_step 11 | --data_path "${{inputs.data_path}}" 12 | 13 | inputs: 14 | data_path: 15 | type: uri_folder 16 | 17 | # What code to make available 18 | code: . 19 | 20 | # Where to run it 21 | environment: 22 | build: 23 | path: ./environment 24 | 25 | -------------------------------------------------------------------------------- /packages/example-reader-step/environment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:jammy AS base 2 | 3 | ARG PYTHON_VERSION=3.12 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | 9 | ENV UV_LINK_MODE="copy" 10 | ENV UV_PYTHON=${PYTHON_VERSION} 11 | COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /bin/ 12 | RUN uv python install ${PYTHON_VERSION} 13 | 14 | 15 | # Stage only used for development 16 | FROM base AS devcontainer 17 | 18 | # Install the Microsoft Linux Repository 19 | RUN curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb && \ 20 | sudo dpkg -i packages-microsoft-prod.deb && \ 21 | rm packages-microsoft-prod.deb && \ 22 | sudo apt-get update 23 | 24 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 25 | && apt-get -y install --no-install-recommends \ 26 | shellcheck \ 27 | azcopy 28 | 29 | 30 | # Stage used in AML. AzureML always builds last stage. 31 | FROM base AS job-runner 32 | 33 | # non-root user, created on base image 34 | USER vscode 35 | ARG VENV_PATH=/home/vscode/venv 36 | 37 | # disable caching as we build once 38 | ENV UV_NO_CACHE=1 39 | 40 | # Create a virtual environment 41 | RUN uv venv ${VENV_PATH} 42 | # Use the virtual environment automatically 43 | ENV VIRTUAL_ENV="${VENV_PATH}" 44 | # Ensure all commands use the virtual environment 45 | ENV PATH="${VENV_PATH}/bin:$PATH" 46 | 47 | # Expect requirements.txt in context for running AzureML jobs 48 | COPY requirements.txt . 49 | RUN uv pip install -r requirements.txt 50 | -------------------------------------------------------------------------------- /packages/example-reader-step/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "example-reader-step" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | dependencies = [ 7 | # needed for logging in AzureML 8 | "azureml-mlflow==1.60.*", 9 | "mlflow-skinny==2.21.*", 10 | "pytz==2025.2", 11 | ] 12 | 13 | [project.scripts] 14 | example-reader-step = "example_reader_step.__main__:main" 15 | 16 | [build-system] 17 | requires = ["hatchling"] 18 | build-backend = "hatchling.build" 19 | -------------------------------------------------------------------------------- /packages/example-reader-step/src/example_reader_step/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bepuca/azureml-scaffolding/d962edbd2edce2fe7c493be31e05e5ca4352c1bb/packages/example-reader-step/src/example_reader_step/__init__.py -------------------------------------------------------------------------------- /packages/example-reader-step/src/example_reader_step/__main__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def read(data_path: Path): 5 | """Read "file.txt" in `data_path` and print its contents.""" 6 | file = data_path / "file.txt" 7 | print(file.read_text()) 8 | 9 | 10 | def main(): 11 | import argparse 12 | import os 13 | import sys 14 | 15 | assume_debug = len(sys.argv) <= 1 16 | if assume_debug: 17 | print("WARNING: Using debug args because no args were passed") 18 | args_dict = { 19 | "data_path": Path(os.environ["REPO_ROOT"]) / "data/outputs", 20 | } 21 | else: 22 | parser = argparse.ArgumentParser(description="Driver script for Example Package") 23 | parser.add_argument("--data_path", type=Path, help="Path to data", required=True) 24 | 25 | args = parser.parse_args() 26 | args_dict = vars(args) 27 | 28 | read(**args_dict) 29 | 30 | 31 | if __name__ == "__main__": 32 | main() 33 | -------------------------------------------------------------------------------- /packages/example-writer-step/.amlignore: -------------------------------------------------------------------------------- 1 | # To prevent unnecessary files from being included in 2 | # the AzureML code snapshot, make an ignore file (.amlignore). 3 | # Place this file in the Snapshot directory and add the 4 | # filenames to ignore in it. The .amlignore file uses 5 | # the same syntax and patterns as the .gitignore file. 6 | # If no .amlignore file is present, the .gitignore file 7 | # is used to determine which files to ignore. 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # Unit test cache 15 | .pytest_cache/ 16 | 17 | # macOS stuff 18 | **/.DS_Store -------------------------------------------------------------------------------- /packages/example-writer-step/README.md: -------------------------------------------------------------------------------- 1 | # example_writer_step 2 | -------------------------------------------------------------------------------- /packages/example-writer-step/aml-component.yaml: -------------------------------------------------------------------------------- 1 | $schema: https://azuremlschemas.azureedge.net/latest/commandComponent.schema.json 2 | type: command 3 | 4 | name: example_writer_step 5 | display_name: Example Writer Step 6 | 7 | # What to run (always cd into src for imports to work) 8 | command: >- 9 | cd src && 10 | python -m example_writer_step 11 | --content "${{ inputs.content }}" 12 | --data_path "${{ outputs.data_path }}" 13 | 14 | inputs: 15 | content: 16 | type: string 17 | 18 | outputs: 19 | data_path: 20 | type: uri_folder 21 | 22 | # What code to make available 23 | code: . 24 | 25 | # Where to run it 26 | environment: 27 | build: 28 | path: ./environment 29 | -------------------------------------------------------------------------------- /packages/example-writer-step/environment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:jammy AS base 2 | 3 | ARG PYTHON_VERSION=3.12 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | 9 | ENV UV_LINK_MODE="copy" 10 | ENV UV_PYTHON=${PYTHON_VERSION} 11 | COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /bin/ 12 | RUN uv python install ${PYTHON_VERSION} 13 | 14 | 15 | # Stage only used for development 16 | FROM base AS devcontainer 17 | 18 | # Install the Microsoft Linux Repository 19 | RUN curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb && \ 20 | sudo dpkg -i packages-microsoft-prod.deb && \ 21 | rm packages-microsoft-prod.deb && \ 22 | sudo apt-get update 23 | 24 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 25 | && apt-get -y install --no-install-recommends \ 26 | shellcheck \ 27 | azcopy 28 | 29 | 30 | # Stage used in AML. AzureML always builds last stage. 31 | FROM base AS job-runner 32 | 33 | # non-root user, created on base image 34 | USER vscode 35 | ARG VENV_PATH=/home/vscode/venv 36 | 37 | # disable caching as we build once 38 | ENV UV_NO_CACHE=1 39 | 40 | # Create a virtual environment 41 | RUN uv venv ${VENV_PATH} 42 | # Use the virtual environment automatically 43 | ENV VIRTUAL_ENV="${VENV_PATH}" 44 | # Ensure all commands use the virtual environment 45 | ENV PATH="${VENV_PATH}/bin:$PATH" 46 | 47 | # Expect requirements.txt in context for running AzureML jobs 48 | COPY requirements.txt . 49 | RUN uv pip install -r requirements.txt 50 | -------------------------------------------------------------------------------- /packages/example-writer-step/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "example-writer-step" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | dependencies = [ 7 | # needed for logging in AzureML 8 | "azureml-mlflow==1.60.*", 9 | "mlflow-skinny==2.21.*", 10 | "pytz==2025.2", 11 | ] 12 | 13 | [project.scripts] 14 | example-writer-step = "example_writer_step.__main__:main" 15 | 16 | [build-system] 17 | requires = ["hatchling"] 18 | build-backend = "hatchling.build" 19 | -------------------------------------------------------------------------------- /packages/example-writer-step/src/example_writer_step/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bepuca/azureml-scaffolding/d962edbd2edce2fe7c493be31e05e5ca4352c1bb/packages/example-writer-step/src/example_writer_step/__init__.py -------------------------------------------------------------------------------- /packages/example-writer-step/src/example_writer_step/__main__.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | 3 | 4 | def write(content: str, data_path: Path): 5 | """Write `content` to "file.txt" in `data_path`""" 6 | data_path.mkdir(parents=True, exist_ok=True) 7 | file = data_path / "file.txt" 8 | file.write_text(content) 9 | 10 | 11 | def main(): 12 | import argparse 13 | import os 14 | import sys 15 | 16 | assume_debug = len(sys.argv) <= 1 17 | if assume_debug: 18 | print("WARNING: Using debug args because no args were passed") 19 | args_dict = { 20 | "content": "Houston we do not have a problem!", 21 | "data_path": Path(os.environ["REPO_ROOT"]) / "data/outputs", 22 | } 23 | else: 24 | parser = argparse.ArgumentParser(description="Driver script for Example Package") 25 | parser.add_argument("--content", type=str, help="Content to write", required=True) 26 | parser.add_argument("--data_path", type=Path, help="Path to data", required=True) 27 | 28 | args = parser.parse_args() 29 | args_dict = vars(args) 30 | 31 | write(**args_dict) 32 | 33 | 34 | if __name__ == "__main__": 35 | main() 36 | -------------------------------------------------------------------------------- /packages/example/.amlignore: -------------------------------------------------------------------------------- 1 | # To prevent unnecessary files from being included in 2 | # the AzureML code snapshot, make an ignore file (.amlignore). 3 | # Place this file in the Snapshot directory and add the 4 | # filenames to ignore in it. The .amlignore file uses 5 | # the same syntax and patterns as the .gitignore file. 6 | # If no .amlignore file is present, the .gitignore file 7 | # is used to determine which files to ignore. 8 | 9 | # Byte-compiled / optimized / DLL files 10 | __pycache__/ 11 | *.py[cod] 12 | *$py.class 13 | 14 | # Unit test cache 15 | .pytest_cache/ 16 | 17 | # macOS stuff 18 | **/.DS_Store -------------------------------------------------------------------------------- /packages/example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | -------------------------------------------------------------------------------- /packages/example/aml-job.yaml: -------------------------------------------------------------------------------- 1 | # Tells Azure ML what kind of YAML this is. 2 | # Docs: https://docs.microsoft.com/en-us/azure/machine-learning/reference-yaml-job-command 3 | $schema: https://azuremlschemas.azureedge.net/latest/commandJob.schema.json 4 | 5 | # Name of the experiment where all jobs will end up in the Azure ML dashboard 6 | experiment_name: example 7 | 8 | # What to run 9 | command: >- 10 | python -m example 11 | --data_path "${{inputs.data_path}}" 12 | --greeting "${{inputs.greeting}}" 13 | 14 | inputs: 15 | data_path: 16 | # Only works if dataset created using Azure ML CLI v2; run `az ml data create --help` to see how 17 | # YAML schema: https://learn.microsoft.com/en-us/azure/machine-learning/reference-yaml-data 18 | type: uri_folder # default, can be changed to `uri_file` if data_path points to a file 19 | path: azureml:: 20 | greeting: "Hello" 21 | 22 | # What code to make available 23 | code: . 24 | 25 | # Where to run it 26 | environment: 27 | build: 28 | path: ./environment 29 | 30 | compute: azureml: 31 | -------------------------------------------------------------------------------- /packages/example/environment/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mcr.microsoft.com/devcontainers/base:jammy AS base 2 | 3 | ARG PYTHON_VERSION=3.12 4 | 5 | # [Optional] Uncomment this section to install additional OS packages. 6 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 7 | # && apt-get -y install --no-install-recommends 8 | 9 | ENV UV_LINK_MODE="copy" 10 | ENV UV_PYTHON=${PYTHON_VERSION} 11 | COPY --from=ghcr.io/astral-sh/uv:0.6.14 /uv /uvx /bin/ 12 | RUN uv python install ${PYTHON_VERSION} 13 | 14 | 15 | # Stage only used for development 16 | FROM base AS devcontainer 17 | 18 | # Install the Microsoft Linux Repository 19 | RUN curl -sSL -O https://packages.microsoft.com/config/ubuntu/22.04/packages-microsoft-prod.deb && \ 20 | sudo dpkg -i packages-microsoft-prod.deb && \ 21 | rm packages-microsoft-prod.deb && \ 22 | sudo apt-get update 23 | 24 | RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 25 | && apt-get -y install --no-install-recommends \ 26 | shellcheck \ 27 | azcopy 28 | 29 | 30 | # Stage used in AML. AzureML always builds last stage. 31 | FROM base AS job-runner 32 | 33 | # non-root user, created on base image 34 | USER vscode 35 | ARG VENV_PATH=/home/vscode/venv 36 | 37 | # disable caching as we build once 38 | ENV UV_NO_CACHE=1 39 | 40 | # Create a virtual environment 41 | RUN uv venv ${VENV_PATH} 42 | # Use the virtual environment automatically 43 | ENV VIRTUAL_ENV="${VENV_PATH}" 44 | # Ensure all commands use the virtual environment 45 | ENV PATH="${VENV_PATH}/bin:$PATH" 46 | 47 | # Expect requirements.txt in context for running AzureML jobs 48 | COPY requirements.txt . 49 | RUN uv pip install -r requirements.txt 50 | -------------------------------------------------------------------------------- /packages/example/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "example" 3 | version = "0.1.0" 4 | description = "Add your description here" 5 | readme = "README.md" 6 | dependencies = [ 7 | # needed for logging in AzureML 8 | "azureml-mlflow==1.60.*", 9 | "mlflow-skinny==2.21.*", 10 | "pytz==2025.2", 11 | ] 12 | 13 | [project.scripts] 14 | example = "example.__main__:main" 15 | 16 | [build-system] 17 | requires = ["hatchling"] 18 | build-backend = "hatchling.build" 19 | -------------------------------------------------------------------------------- /packages/example/src/example/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bepuca/azureml-scaffolding/d962edbd2edce2fe7c493be31e05e5ca4352c1bb/packages/example/src/example/__init__.py -------------------------------------------------------------------------------- /packages/example/src/example/__main__.py: -------------------------------------------------------------------------------- 1 | import sys 2 | from pathlib import Path 3 | 4 | from example.answer import get_the_ultimate_answer 5 | from shared.logging import azureml_logger 6 | 7 | 8 | def example(data_path: Path, greeting: str = "Hello"): 9 | """Example function with the main things needed to get started with AzureML 10 | 11 | :param data_path: Path where data is stored. Here to exemplify how to connect AzureML data 12 | (see `aml-job.yaml`). 13 | :param greeting: Word with which to greet the world. 14 | """ 15 | 16 | # Tags are shown as properties of the job in the Azure ML dashboard. Run once. 17 | # Good practice to set input values as tags for reproducibility 18 | tags = {"greeting": greeting} 19 | azureml_logger.set_tags(tags) 20 | 21 | # Console logs in AzureML are captured from stdout and stderr 22 | print(f"{greeting} world!") 23 | print(f"'data_path' is pointing to '{data_path}'", file=sys.stderr) 24 | if isinstance(data_path, Path) and data_path.is_dir(): 25 | for file in data_path.iterdir(): 26 | print(f" {file.relative_to(data_path)}", file=sys.stderr) 27 | 28 | # Metrics are numerical values 29 | metrics = {"answer": get_the_ultimate_answer()} 30 | azureml_logger.log_metrics(metrics) 31 | 32 | values = [1.0, 0.0, 1.0, 2.0, 3.0, 2.0, 4.0] 33 | for v in values: 34 | metrics = {"value": v} 35 | azureml_logger.log_metrics(metrics) 36 | 37 | 38 | def main(): 39 | import argparse 40 | import sys 41 | from pathlib import Path 42 | 43 | assume_debug = len(sys.argv) <= 1 44 | if assume_debug: 45 | print("WARNING: Using debug args because no args were passed") 46 | args_dict = { 47 | "data_path": Path("path/to/data"), 48 | "greeting": "Hello", 49 | } 50 | else: 51 | parser = argparse.ArgumentParser(description="Driver script for Example Package") 52 | parser.add_argument("--data_path", type=Path, help="Path to data", required=True) 53 | parser.add_argument("--greeting", type=str, help="Greeting word", required=True) 54 | 55 | args = parser.parse_args() 56 | args_dict = vars(args) 57 | 58 | example(**args_dict) 59 | 60 | 61 | if __name__ == "__main__": 62 | main() 63 | -------------------------------------------------------------------------------- /packages/example/src/example/answer.py: -------------------------------------------------------------------------------- 1 | def get_the_ultimate_answer() -> float: 2 | """Dummy function to exemplify unit tests""" 3 | return 42.0 4 | -------------------------------------------------------------------------------- /packages/example/tests/test_answer.py: -------------------------------------------------------------------------------- 1 | from example.answer import get_the_ultimate_answer 2 | 3 | 4 | def test_get_the_ultimate_answer(): 5 | expected_answer = 42 6 | 7 | answer = get_the_ultimate_answer() 8 | 9 | assert answer == expected_answer 10 | -------------------------------------------------------------------------------- /packages/shared/README.md: -------------------------------------------------------------------------------- 1 | # Shared 2 | 3 | This is a special package that contains code that may be shared across two 4 | or more other packages in the workspaces. The functionality in `bin` ensures it 5 | is used properly in AzureML. For local development, it is simply installed in 6 | the environment as an editable package. 7 | 8 | The package should not have any dependencies on its own. The dependencies 9 | should be defined by each package. That way, we can allow runs to 10 | not have all dependencies that are used in `shared` if some of them are not 11 | necessary. 12 | -------------------------------------------------------------------------------- /packages/shared/pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "shared" 3 | version = "0.1.0" 4 | description = "Package shared across the workspace" 5 | readme = "README.md" 6 | dependencies = [] 7 | 8 | 9 | [build-system] 10 | requires = ["hatchling"] 11 | build-backend = "hatchling.build" 12 | -------------------------------------------------------------------------------- /packages/shared/src/shared/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bepuca/azureml-scaffolding/d962edbd2edce2fe7c493be31e05e5ca4352c1bb/packages/shared/src/shared/__init__.py -------------------------------------------------------------------------------- /packages/shared/src/shared/logging/azureml_logger.py: -------------------------------------------------------------------------------- 1 | import os 2 | import sys 3 | 4 | # "MLFLOW_TRACKING_URI" is set-up when running inside an Azure ML Job 5 | 6 | if tracking_uri := os.getenv("MLFLOW_TRACKING_URI", None): 7 | import mlflow 8 | 9 | mlflow.set_tracking_uri(tracking_uri) 10 | 11 | def set_tags(tags: dict[str, str]) -> None: 12 | mlflow.set_tags(tags) 13 | 14 | def log_metrics(metrics: dict[str, int | float]) -> None: 15 | mlflow.log_metrics(metrics) 16 | 17 | else: 18 | 19 | def set_tags(tags: dict[str, str]) -> None: 20 | for name, value in tags.items(): 21 | print(f"tag:{name}={value}", file=sys.stderr) 22 | 23 | def log_metrics(metrics: dict[str, int | float]) -> None: 24 | for name, value in metrics.items(): 25 | print(f"metric:{name}={value}", file=sys.stderr) 26 | -------------------------------------------------------------------------------- /packages/shared/tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bepuca/azureml-scaffolding/d962edbd2edce2fe7c493be31e05e5ca4352c1bb/packages/shared/tests/__init__.py -------------------------------------------------------------------------------- /pipelines/example-pipeline.yaml: -------------------------------------------------------------------------------- 1 | $schema: https://azuremlschemas.azureedge.net/latest/pipelineJob.schema.json 2 | type: pipeline 3 | 4 | experiment_name: example-pipeline 5 | settings: 6 | default_compute: azureml: 7 | 8 | inputs: 9 | content: "hello world" 10 | 11 | jobs: 12 | # always keep the snapshot step so pipeline YAML makes it into the jobs 13 | snapshot: 14 | name: Snapshot 15 | command: echo "Uploading full pipeline snapshot" 16 | code: . 17 | environment: 18 | image: mcr.microsoft.com/azureml/inference-base-2204 19 | 20 | example_writer_step: 21 | type: command 22 | component: ./example-writer-step/aml-component.yaml 23 | inputs: 24 | content: ${{ parent.inputs.content }} 25 | outputs: 26 | data_path: 27 | type: uri_folder 28 | mode: upload 29 | 30 | example_reader_step: 31 | type: command 32 | component: ./example-reader-step/aml-component.yaml 33 | inputs: 34 | data_path: ${{ parent.jobs.example_writer_step.outputs.data_path }} -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "scaffolding" 3 | version = "0.2.0" 4 | description = "" 5 | readme = "README.md" 6 | requires-python = "==3.12.*" 7 | dependencies = [ 8 | "example", 9 | "example-reader-step", 10 | "example-writer-step", 11 | "shared", 12 | ] 13 | 14 | [dependency-groups] 15 | dev = [ 16 | "pytest ==8.3.*", 17 | "pytest-cov ==6.1.*", 18 | "pyright ==1.1.*", 19 | "ruff ==0.11.*", 20 | "pre-commit ==4.2.*" 21 | ] 22 | 23 | [tool.pyright] 24 | pythonVersion = "3.12" 25 | typeCheckingMode = "basic" 26 | 27 | [tool.ruff] 28 | src = ["packages/**/src"] 29 | line-length = 100 30 | target-version = "py312" 31 | show-fixes = true 32 | lint.select = [ 33 | "B0", # bugbear 34 | "E", # pycodestyle 35 | "F", # Pyflakes 36 | "I", # import order 37 | "UP", # pyupgrade 38 | "RUF100", # valid noqa annnotations 39 | ] 40 | 41 | [tool.pytest.ini_options] 42 | testpaths = ["packages"] 43 | addopts = "--strict-markers --import-mode=importlib" 44 | 45 | [tool.coverage.run] 46 | omit = ["*/tests/*"] 47 | 48 | [tool.uv] 49 | environments = ["sys_platform == 'linux'"] 50 | 51 | [tool.uv.workspace] 52 | members = ["packages/*"] 53 | 54 | [tool.uv.sources] 55 | shared = { workspace = true } 56 | example = { workspace = true } 57 | example-writer-step = { workspace = true } 58 | example-reader-step = { workspace = true } 59 | 60 | -------------------------------------------------------------------------------- /runs/.gitignore: -------------------------------------------------------------------------------- 1 | * 2 | !.gitignore -------------------------------------------------------------------------------- /uv.lock: -------------------------------------------------------------------------------- 1 | version = 1 2 | revision = 1 3 | requires-python = "==3.12.*" 4 | resolution-markers = [ 5 | "sys_platform == 'linux'", 6 | ] 7 | supported-markers = [ 8 | "sys_platform == 'linux'", 9 | ] 10 | 11 | [manifest] 12 | members = [ 13 | "example", 14 | "example-reader-step", 15 | "example-writer-step", 16 | "scaffolding", 17 | "shared", 18 | ] 19 | 20 | [[package]] 21 | name = "annotated-types" 22 | version = "0.7.0" 23 | source = { registry = "https://pypi.org/simple" } 24 | sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 } 25 | wheels = [ 26 | { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 }, 27 | ] 28 | 29 | [[package]] 30 | name = "anyio" 31 | version = "4.9.0" 32 | source = { registry = "https://pypi.org/simple" } 33 | dependencies = [ 34 | { name = "idna", marker = "sys_platform == 'linux'" }, 35 | { name = "sniffio", marker = "sys_platform == 'linux'" }, 36 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 37 | ] 38 | sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 } 39 | wheels = [ 40 | { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 }, 41 | ] 42 | 43 | [[package]] 44 | name = "azure-common" 45 | version = "1.1.28" 46 | source = { registry = "https://pypi.org/simple" } 47 | sdist = { url = "https://files.pythonhosted.org/packages/3e/71/f6f71a276e2e69264a97ad39ef850dca0a04fce67b12570730cb38d0ccac/azure-common-1.1.28.zip", hash = "sha256:4ac0cd3214e36b6a1b6a442686722a5d8cc449603aa833f3f0f40bda836704a3", size = 20914 } 48 | wheels = [ 49 | { url = "https://files.pythonhosted.org/packages/62/55/7f118b9c1b23ec15ca05d15a578d8207aa1706bc6f7c87218efffbbf875d/azure_common-1.1.28-py2.py3-none-any.whl", hash = "sha256:5c12d3dcf4ec20599ca6b0d3e09e86e146353d443e7fcc050c9a19c1f9df20ad", size = 14462 }, 50 | ] 51 | 52 | [[package]] 53 | name = "azure-core" 54 | version = "1.33.0" 55 | source = { registry = "https://pypi.org/simple" } 56 | dependencies = [ 57 | { name = "requests", marker = "sys_platform == 'linux'" }, 58 | { name = "six", marker = "sys_platform == 'linux'" }, 59 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 60 | ] 61 | sdist = { url = "https://files.pythonhosted.org/packages/75/aa/7c9db8edd626f1a7d99d09ef7926f6f4fb34d5f9fa00dc394afdfe8e2a80/azure_core-1.33.0.tar.gz", hash = "sha256:f367aa07b5e3005fec2c1e184b882b0b039910733907d001c20fb08ebb8c0eb9", size = 295633 } 62 | wheels = [ 63 | { url = "https://files.pythonhosted.org/packages/07/b7/76b7e144aa53bd206bf1ce34fa75350472c3f69bf30e5c8c18bc9881035d/azure_core-1.33.0-py3-none-any.whl", hash = "sha256:9b5b6d0223a1d38c37500e6971118c1e0f13f54951e6893968b38910bc9cda8f", size = 207071 }, 64 | ] 65 | 66 | [[package]] 67 | name = "azure-identity" 68 | version = "1.21.0" 69 | source = { registry = "https://pypi.org/simple" } 70 | dependencies = [ 71 | { name = "azure-core", marker = "sys_platform == 'linux'" }, 72 | { name = "cryptography", marker = "sys_platform == 'linux'" }, 73 | { name = "msal", marker = "sys_platform == 'linux'" }, 74 | { name = "msal-extensions", marker = "sys_platform == 'linux'" }, 75 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 76 | ] 77 | sdist = { url = "https://files.pythonhosted.org/packages/b5/a1/f1a683672e7a88ea0e3119f57b6c7843ed52650fdcac8bfa66ed84e86e40/azure_identity-1.21.0.tar.gz", hash = "sha256:ea22ce6e6b0f429bc1b8d9212d5b9f9877bd4c82f1724bfa910760612c07a9a6", size = 266445 } 78 | wheels = [ 79 | { url = "https://files.pythonhosted.org/packages/3d/9f/1f9f3ef4f49729ee207a712a5971a9ca747f2ca47d9cbf13cf6953e3478a/azure_identity-1.21.0-py3-none-any.whl", hash = "sha256:258ea6325537352440f71b35c3dffe9d240eae4a5126c1b7ce5efd5766bd9fd9", size = 189190 }, 80 | ] 81 | 82 | [[package]] 83 | name = "azure-mgmt-core" 84 | version = "1.5.0" 85 | source = { registry = "https://pypi.org/simple" } 86 | dependencies = [ 87 | { name = "azure-core", marker = "sys_platform == 'linux'" }, 88 | ] 89 | sdist = { url = "https://files.pythonhosted.org/packages/48/9a/9bdc35295a16fe9139a1f99c13d9915563cbc4f30b479efaa40f8694eaf7/azure_mgmt_core-1.5.0.tar.gz", hash = "sha256:380ae3dfa3639f4a5c246a7db7ed2d08374e88230fd0da3eb899f7c11e5c441a", size = 32093 } 90 | wheels = [ 91 | { url = "https://files.pythonhosted.org/packages/ab/2d/762b027cfd58b1b2c9b5b60d112615bd04bc33ef85dac55d2ee739641054/azure_mgmt_core-1.5.0-py3-none-any.whl", hash = "sha256:18aaa5a723ee8ae05bf1bfc9f6d0ffb996631c7ea3c922cc86f522973ce07b5f", size = 30295 }, 92 | ] 93 | 94 | [[package]] 95 | name = "azure-storage-blob" 96 | version = "12.19.0" 97 | source = { registry = "https://pypi.org/simple" } 98 | dependencies = [ 99 | { name = "azure-core", marker = "sys_platform == 'linux'" }, 100 | { name = "cryptography", marker = "sys_platform == 'linux'" }, 101 | { name = "isodate", marker = "sys_platform == 'linux'" }, 102 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 103 | ] 104 | sdist = { url = "https://files.pythonhosted.org/packages/fd/f8/59c209132b3b2993402df6b7e79728726927b53168624e917cd9daaffea8/azure-storage-blob-12.19.0.tar.gz", hash = "sha256:26c0a4320a34a3c2a1b74528ba6812ebcb632a04cd67b1c7377232c4b01a5897", size = 551109 } 105 | wheels = [ 106 | { url = "https://files.pythonhosted.org/packages/f6/82/24b0d7cf67ea63af86f11092756b8fe2adc1d55323241dc4107f5f5748e2/azure_storage_blob-12.19.0-py3-none-any.whl", hash = "sha256:7bbc2c9c16678f7a420367fef6b172ba8730a7e66df7f4d7a55d5b3c8216615b", size = 394221 }, 107 | ] 108 | 109 | [[package]] 110 | name = "azureml-mlflow" 111 | version = "1.60.0" 112 | source = { registry = "https://pypi.org/simple" } 113 | dependencies = [ 114 | { name = "azure-common", marker = "sys_platform == 'linux'" }, 115 | { name = "azure-core", marker = "sys_platform == 'linux'" }, 116 | { name = "azure-identity", marker = "sys_platform == 'linux'" }, 117 | { name = "azure-mgmt-core", marker = "sys_platform == 'linux'" }, 118 | { name = "azure-storage-blob", marker = "sys_platform == 'linux'" }, 119 | { name = "cryptography", marker = "sys_platform == 'linux'" }, 120 | { name = "jsonpickle", marker = "sys_platform == 'linux'" }, 121 | { name = "mlflow-skinny", marker = "sys_platform == 'linux'" }, 122 | { name = "msrest", marker = "sys_platform == 'linux'" }, 123 | { name = "python-dateutil", marker = "sys_platform == 'linux'" }, 124 | { name = "pytz", marker = "sys_platform == 'linux'" }, 125 | ] 126 | wheels = [ 127 | { url = "https://files.pythonhosted.org/packages/6f/cb/b1f1ac36949958a15707f0d0f361e0a610523dc44cf82e64a404acb9797c/azureml_mlflow-1.60.0-py3-none-any.whl", hash = "sha256:9074fa389cf24f16f3aff7d7cda62a658c93b65a4aecc3dd50a5f1e45909687f", size = 1020403 }, 128 | ] 129 | 130 | [[package]] 131 | name = "cachetools" 132 | version = "5.5.2" 133 | source = { registry = "https://pypi.org/simple" } 134 | sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 } 135 | wheels = [ 136 | { url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 }, 137 | ] 138 | 139 | [[package]] 140 | name = "certifi" 141 | version = "2025.1.31" 142 | source = { registry = "https://pypi.org/simple" } 143 | sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 } 144 | wheels = [ 145 | { url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 }, 146 | ] 147 | 148 | [[package]] 149 | name = "cffi" 150 | version = "1.17.1" 151 | source = { registry = "https://pypi.org/simple" } 152 | dependencies = [ 153 | { name = "pycparser", marker = "sys_platform == 'linux'" }, 154 | ] 155 | sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 } 156 | wheels = [ 157 | { url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 }, 158 | { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 }, 159 | { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 }, 160 | { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 }, 161 | { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 }, 162 | { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 }, 163 | { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 }, 164 | ] 165 | 166 | [[package]] 167 | name = "cfgv" 168 | version = "3.4.0" 169 | source = { registry = "https://pypi.org/simple" } 170 | sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114 } 171 | wheels = [ 172 | { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249 }, 173 | ] 174 | 175 | [[package]] 176 | name = "charset-normalizer" 177 | version = "3.4.1" 178 | source = { registry = "https://pypi.org/simple" } 179 | sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 } 180 | wheels = [ 181 | { url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 }, 182 | { url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 }, 183 | { url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 }, 184 | { url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 }, 185 | { url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 }, 186 | { url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 }, 187 | { url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 }, 188 | { url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 }, 189 | { url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 }, 190 | { url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 }, 191 | { url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 }, 192 | ] 193 | 194 | [[package]] 195 | name = "click" 196 | version = "8.1.8" 197 | source = { registry = "https://pypi.org/simple" } 198 | sdist = { url = "https://files.pythonhosted.org/packages/b9/2e/0090cbf739cee7d23781ad4b89a9894a41538e4fcf4c31dcdd705b78eb8b/click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a", size = 226593 } 199 | wheels = [ 200 | { url = "https://files.pythonhosted.org/packages/7e/d4/7ebdbd03970677812aac39c869717059dbb71a4cfc033ca6e5221787892c/click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2", size = 98188 }, 201 | ] 202 | 203 | [[package]] 204 | name = "cloudpickle" 205 | version = "3.1.1" 206 | source = { registry = "https://pypi.org/simple" } 207 | sdist = { url = "https://files.pythonhosted.org/packages/52/39/069100b84d7418bc358d81669d5748efb14b9cceacd2f9c75f550424132f/cloudpickle-3.1.1.tar.gz", hash = "sha256:b216fa8ae4019d5482a8ac3c95d8f6346115d8835911fd4aefd1a445e4242c64", size = 22113 } 208 | wheels = [ 209 | { url = "https://files.pythonhosted.org/packages/7e/e8/64c37fadfc2816a7701fa8a6ed8d87327c7d54eacfbfb6edab14a2f2be75/cloudpickle-3.1.1-py3-none-any.whl", hash = "sha256:c8c5a44295039331ee9dad40ba100a9c7297b6f988e50e87ccdf3765a668350e", size = 20992 }, 210 | ] 211 | 212 | [[package]] 213 | name = "coverage" 214 | version = "7.6.12" 215 | source = { registry = "https://pypi.org/simple" } 216 | sdist = { url = "https://files.pythonhosted.org/packages/0c/d6/2b53ab3ee99f2262e6f0b8369a43f6d66658eab45510331c0b3d5c8c4272/coverage-7.6.12.tar.gz", hash = "sha256:48cfc4641d95d34766ad41d9573cc0f22a48aa88d22657a1fe01dca0dbae4de2", size = 805941 } 217 | wheels = [ 218 | { url = "https://files.pythonhosted.org/packages/bd/10/fecabcf438ba676f706bf90186ccf6ff9f6158cc494286965c76e58742fa/coverage-7.6.12-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0e549f54ac5f301e8e04c569dfdb907f7be71b06b88b5063ce9d6953d2d58574", size = 242987 }, 219 | { url = "https://files.pythonhosted.org/packages/4c/53/4e208440389e8ea936f5f2b0762dcd4cb03281a7722def8e2bf9dc9c3d68/coverage-7.6.12-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:959244a17184515f8c52dcb65fb662808767c0bd233c1d8a166e7cf74c9ea985", size = 239881 }, 220 | { url = "https://files.pythonhosted.org/packages/c4/47/2ba744af8d2f0caa1f17e7746147e34dfc5f811fb65fc153153722d58835/coverage-7.6.12-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bda1c5f347550c359f841d6614fb8ca42ae5cb0b74d39f8a1e204815ebe25750", size = 242142 }, 221 | { url = "https://files.pythonhosted.org/packages/e9/90/df726af8ee74d92ee7e3bf113bf101ea4315d71508952bd21abc3fae471e/coverage-7.6.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ceeb90c3eda1f2d8c4c578c14167dbd8c674ecd7d38e45647543f19839dd6ea", size = 241437 }, 222 | { url = "https://files.pythonhosted.org/packages/f6/af/995263fd04ae5f9cf12521150295bf03b6ba940d0aea97953bb4a6db3e2b/coverage-7.6.12-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f16f44025c06792e0fb09571ae454bcc7a3ec75eeb3c36b025eccf501b1a4c3", size = 239724 }, 223 | { url = "https://files.pythonhosted.org/packages/1c/8e/5bb04f0318805e190984c6ce106b4c3968a9562a400180e549855d8211bd/coverage-7.6.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b076e625396e787448d27a411aefff867db2bffac8ed04e8f7056b07024eed5a", size = 241329 }, 224 | { url = "https://files.pythonhosted.org/packages/fb/b2/f655700e1024dec98b10ebaafd0cedbc25e40e4abe62a3c8e2ceef4f8f0a/coverage-7.6.12-py3-none-any.whl", hash = "sha256:eb8668cfbc279a536c633137deeb9435d2962caec279c3f8cf8b91fff6ff8953", size = 200552 }, 225 | ] 226 | 227 | [[package]] 228 | name = "cryptography" 229 | version = "44.0.2" 230 | source = { registry = "https://pypi.org/simple" } 231 | dependencies = [ 232 | { name = "cffi", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'linux'" }, 233 | ] 234 | sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 } 235 | wheels = [ 236 | { url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 }, 237 | { url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 }, 238 | { url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 }, 239 | { url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 }, 240 | { url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 }, 241 | { url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 }, 242 | { url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 }, 243 | { url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 }, 244 | { url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 }, 245 | { url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 }, 246 | { url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 }, 247 | { url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 }, 248 | { url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 }, 249 | { url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 }, 250 | { url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 }, 251 | { url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 }, 252 | { url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 }, 253 | { url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 }, 254 | ] 255 | 256 | [[package]] 257 | name = "databricks-sdk" 258 | version = "0.50.0" 259 | source = { registry = "https://pypi.org/simple" } 260 | dependencies = [ 261 | { name = "google-auth", marker = "sys_platform == 'linux'" }, 262 | { name = "requests", marker = "sys_platform == 'linux'" }, 263 | ] 264 | sdist = { url = "https://files.pythonhosted.org/packages/90/67/b1e1dff8f661c33e0ce0fb518e09beb460e9e1b92da237e43f7e89718da3/databricks_sdk-0.50.0.tar.gz", hash = "sha256:485a604389fad7e9e26c7c4aeeebab3f486e7740c3f54ed64a13cbec1adbd0c0", size = 731523 } 265 | wheels = [ 266 | { url = "https://files.pythonhosted.org/packages/95/ae/e6b2a98df2dcc743b71814a15bf7c6744acb8f2893e7c52cb9b75b305fcd/databricks_sdk-0.50.0-py3-none-any.whl", hash = "sha256:fa4c0b2a549d660a71432702da23197860c6f6d72320f326f8007257496a0a0a", size = 692306 }, 267 | ] 268 | 269 | [[package]] 270 | name = "deprecated" 271 | version = "1.2.18" 272 | source = { registry = "https://pypi.org/simple" } 273 | dependencies = [ 274 | { name = "wrapt", marker = "sys_platform == 'linux'" }, 275 | ] 276 | sdist = { url = "https://files.pythonhosted.org/packages/98/97/06afe62762c9a8a86af0cfb7bfdab22a43ad17138b07af5b1a58442690a2/deprecated-1.2.18.tar.gz", hash = "sha256:422b6f6d859da6f2ef57857761bfb392480502a64c3028ca9bbe86085d72115d", size = 2928744 } 277 | wheels = [ 278 | { url = "https://files.pythonhosted.org/packages/6e/c6/ac0b6c1e2d138f1002bcf799d330bd6d85084fece321e662a14223794041/Deprecated-1.2.18-py2.py3-none-any.whl", hash = "sha256:bd5011788200372a32418f888e326a09ff80d0214bd961147cfed01b5c018eec", size = 9998 }, 279 | ] 280 | 281 | [[package]] 282 | name = "distlib" 283 | version = "0.3.9" 284 | source = { registry = "https://pypi.org/simple" } 285 | sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923 } 286 | wheels = [ 287 | { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973 }, 288 | ] 289 | 290 | [[package]] 291 | name = "example" 292 | version = "0.1.0" 293 | source = { editable = "packages/example" } 294 | dependencies = [ 295 | { name = "azureml-mlflow", marker = "sys_platform == 'linux'" }, 296 | { name = "mlflow-skinny", marker = "sys_platform == 'linux'" }, 297 | { name = "pytz", marker = "sys_platform == 'linux'" }, 298 | ] 299 | 300 | [package.metadata] 301 | requires-dist = [ 302 | { name = "azureml-mlflow", specifier = "==1.60.*" }, 303 | { name = "mlflow-skinny", specifier = "==2.21.*" }, 304 | { name = "pytz", specifier = "==2025.2" }, 305 | ] 306 | 307 | [[package]] 308 | name = "example-reader-step" 309 | version = "0.1.0" 310 | source = { editable = "packages/example-reader-step" } 311 | dependencies = [ 312 | { name = "azureml-mlflow", marker = "sys_platform == 'linux'" }, 313 | { name = "mlflow-skinny", marker = "sys_platform == 'linux'" }, 314 | { name = "pytz", marker = "sys_platform == 'linux'" }, 315 | ] 316 | 317 | [package.metadata] 318 | requires-dist = [ 319 | { name = "azureml-mlflow", specifier = "==1.60.*" }, 320 | { name = "mlflow-skinny", specifier = "==2.21.*" }, 321 | { name = "pytz", specifier = "==2025.2" }, 322 | ] 323 | 324 | [[package]] 325 | name = "example-writer-step" 326 | version = "0.1.0" 327 | source = { editable = "packages/example-writer-step" } 328 | dependencies = [ 329 | { name = "azureml-mlflow", marker = "sys_platform == 'linux'" }, 330 | { name = "mlflow-skinny", marker = "sys_platform == 'linux'" }, 331 | { name = "pytz", marker = "sys_platform == 'linux'" }, 332 | ] 333 | 334 | [package.metadata] 335 | requires-dist = [ 336 | { name = "azureml-mlflow", specifier = "==1.60.*" }, 337 | { name = "mlflow-skinny", specifier = "==2.21.*" }, 338 | { name = "pytz", specifier = "==2025.2" }, 339 | ] 340 | 341 | [[package]] 342 | name = "fastapi" 343 | version = "0.115.12" 344 | source = { registry = "https://pypi.org/simple" } 345 | dependencies = [ 346 | { name = "pydantic", marker = "sys_platform == 'linux'" }, 347 | { name = "starlette", marker = "sys_platform == 'linux'" }, 348 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 349 | ] 350 | sdist = { url = "https://files.pythonhosted.org/packages/f4/55/ae499352d82338331ca1e28c7f4a63bfd09479b16395dce38cf50a39e2c2/fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681", size = 295236 } 351 | wheels = [ 352 | { url = "https://files.pythonhosted.org/packages/50/b3/b51f09c2ba432a576fe63758bddc81f78f0c6309d9e5c10d194313bf021e/fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d", size = 95164 }, 353 | ] 354 | 355 | [[package]] 356 | name = "filelock" 357 | version = "3.17.0" 358 | source = { registry = "https://pypi.org/simple" } 359 | sdist = { url = "https://files.pythonhosted.org/packages/dc/9c/0b15fb47b464e1b663b1acd1253a062aa5feecb07d4e597daea542ebd2b5/filelock-3.17.0.tar.gz", hash = "sha256:ee4e77401ef576ebb38cd7f13b9b28893194acc20a8e68e18730ba9c0e54660e", size = 18027 } 360 | wheels = [ 361 | { url = "https://files.pythonhosted.org/packages/89/ec/00d68c4ddfedfe64159999e5f8a98fb8442729a63e2077eb9dcd89623d27/filelock-3.17.0-py3-none-any.whl", hash = "sha256:533dc2f7ba78dc2f0f531fc6c4940addf7b70a481e269a5a3b93be94ffbe8338", size = 16164 }, 362 | ] 363 | 364 | [[package]] 365 | name = "gitdb" 366 | version = "4.0.12" 367 | source = { registry = "https://pypi.org/simple" } 368 | dependencies = [ 369 | { name = "smmap", marker = "sys_platform == 'linux'" }, 370 | ] 371 | sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684 } 372 | wheels = [ 373 | { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794 }, 374 | ] 375 | 376 | [[package]] 377 | name = "gitpython" 378 | version = "3.1.44" 379 | source = { registry = "https://pypi.org/simple" } 380 | dependencies = [ 381 | { name = "gitdb", marker = "sys_platform == 'linux'" }, 382 | ] 383 | sdist = { url = "https://files.pythonhosted.org/packages/c0/89/37df0b71473153574a5cdef8f242de422a0f5d26d7a9e231e6f169b4ad14/gitpython-3.1.44.tar.gz", hash = "sha256:c87e30b26253bf5418b01b0660f818967f3c503193838337fe5e573331249269", size = 214196 } 384 | wheels = [ 385 | { url = "https://files.pythonhosted.org/packages/1d/9a/4114a9057db2f1462d5c8f8390ab7383925fe1ac012eaa42402ad65c2963/GitPython-3.1.44-py3-none-any.whl", hash = "sha256:9e0e10cda9bed1ee64bc9a6de50e7e38a9c9943241cd7f585f6df3ed28011110", size = 207599 }, 386 | ] 387 | 388 | [[package]] 389 | name = "google-auth" 390 | version = "2.39.0" 391 | source = { registry = "https://pypi.org/simple" } 392 | dependencies = [ 393 | { name = "cachetools", marker = "sys_platform == 'linux'" }, 394 | { name = "pyasn1-modules", marker = "sys_platform == 'linux'" }, 395 | { name = "rsa", marker = "sys_platform == 'linux'" }, 396 | ] 397 | sdist = { url = "https://files.pythonhosted.org/packages/cb/8e/8f45c9a32f73e786e954b8f9761c61422955d23c45d1e8c347f9b4b59e8e/google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7", size = 274834 } 398 | wheels = [ 399 | { url = "https://files.pythonhosted.org/packages/ce/12/ad37a1ef86006d0a0117fc06a4a00bd461c775356b534b425f00dde208ea/google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2", size = 212319 }, 400 | ] 401 | 402 | [[package]] 403 | name = "h11" 404 | version = "0.14.0" 405 | source = { registry = "https://pypi.org/simple" } 406 | sdist = { url = "https://files.pythonhosted.org/packages/f5/38/3af3d3633a34a3316095b39c8e8fb4853a28a536e55d347bd8d8e9a14b03/h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d", size = 100418 } 407 | wheels = [ 408 | { url = "https://files.pythonhosted.org/packages/95/04/ff642e65ad6b90db43e668d70ffb6736436c7ce41fcc549f4e9472234127/h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761", size = 58259 }, 409 | ] 410 | 411 | [[package]] 412 | name = "identify" 413 | version = "2.6.9" 414 | source = { registry = "https://pypi.org/simple" } 415 | sdist = { url = "https://files.pythonhosted.org/packages/9b/98/a71ab060daec766acc30fb47dfca219d03de34a70d616a79a38c6066c5bf/identify-2.6.9.tar.gz", hash = "sha256:d40dfe3142a1421d8518e3d3985ef5ac42890683e32306ad614a29490abeb6bf", size = 99249 } 416 | wheels = [ 417 | { url = "https://files.pythonhosted.org/packages/07/ce/0845144ed1f0e25db5e7a79c2354c1da4b5ce392b8966449d5db8dca18f1/identify-2.6.9-py2.py3-none-any.whl", hash = "sha256:c98b4322da415a8e5a70ff6e51fbc2d2932c015532d77e9f8537b4ba7813b150", size = 99101 }, 418 | ] 419 | 420 | [[package]] 421 | name = "idna" 422 | version = "3.10" 423 | source = { registry = "https://pypi.org/simple" } 424 | sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 } 425 | wheels = [ 426 | { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 }, 427 | ] 428 | 429 | [[package]] 430 | name = "importlib-metadata" 431 | version = "8.6.1" 432 | source = { registry = "https://pypi.org/simple" } 433 | dependencies = [ 434 | { name = "zipp", marker = "sys_platform == 'linux'" }, 435 | ] 436 | sdist = { url = "https://files.pythonhosted.org/packages/33/08/c1395a292bb23fd03bdf572a1357c5a733d3eecbab877641ceacab23db6e/importlib_metadata-8.6.1.tar.gz", hash = "sha256:310b41d755445d74569f993ccfc22838295d9fe005425094fad953d7f15c8580", size = 55767 } 437 | wheels = [ 438 | { url = "https://files.pythonhosted.org/packages/79/9d/0fb148dc4d6fa4a7dd1d8378168d9b4cd8d4560a6fbf6f0121c5fc34eb68/importlib_metadata-8.6.1-py3-none-any.whl", hash = "sha256:02a89390c1e15fdfdc0d7c6b25cb3e62650d0494005c97d6f148bf5b9787525e", size = 26971 }, 439 | ] 440 | 441 | [[package]] 442 | name = "iniconfig" 443 | version = "2.0.0" 444 | source = { registry = "https://pypi.org/simple" } 445 | sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } 446 | wheels = [ 447 | { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, 448 | ] 449 | 450 | [[package]] 451 | name = "isodate" 452 | version = "0.7.2" 453 | source = { registry = "https://pypi.org/simple" } 454 | sdist = { url = "https://files.pythonhosted.org/packages/54/4d/e940025e2ce31a8ce1202635910747e5a87cc3a6a6bb2d00973375014749/isodate-0.7.2.tar.gz", hash = "sha256:4cd1aa0f43ca76f4a6c6c0292a85f40b35ec2e43e315b59f06e6d32171a953e6", size = 29705 } 455 | wheels = [ 456 | { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320 }, 457 | ] 458 | 459 | [[package]] 460 | name = "jsonpickle" 461 | version = "4.0.5" 462 | source = { registry = "https://pypi.org/simple" } 463 | sdist = { url = "https://files.pythonhosted.org/packages/d6/33/4bda317ab294722fcdfff8f63aab74af9fda3675a4652d984a101aa7587e/jsonpickle-4.0.5.tar.gz", hash = "sha256:f299818b39367c361b3f26bdba827d4249ab5d383cd93144d0f94b5417aacb35", size = 315661 } 464 | wheels = [ 465 | { url = "https://files.pythonhosted.org/packages/dc/1b/0e79cf115e0f54f1e8f56effb6ffd2ef8f92e9c324d692ede660067f1bfe/jsonpickle-4.0.5-py3-none-any.whl", hash = "sha256:b4ac7d0a75ddcdfd93445737f1d36ff28768690d43e54bf5d0ddb1d915e580df", size = 46382 }, 466 | ] 467 | 468 | [[package]] 469 | name = "mlflow-skinny" 470 | version = "2.21.3" 471 | source = { registry = "https://pypi.org/simple" } 472 | dependencies = [ 473 | { name = "cachetools", marker = "sys_platform == 'linux'" }, 474 | { name = "click", marker = "sys_platform == 'linux'" }, 475 | { name = "cloudpickle", marker = "sys_platform == 'linux'" }, 476 | { name = "databricks-sdk", marker = "sys_platform == 'linux'" }, 477 | { name = "fastapi", marker = "sys_platform == 'linux'" }, 478 | { name = "gitpython", marker = "sys_platform == 'linux'" }, 479 | { name = "importlib-metadata", marker = "sys_platform == 'linux'" }, 480 | { name = "opentelemetry-api", marker = "sys_platform == 'linux'" }, 481 | { name = "opentelemetry-sdk", marker = "sys_platform == 'linux'" }, 482 | { name = "packaging", marker = "sys_platform == 'linux'" }, 483 | { name = "protobuf", marker = "sys_platform == 'linux'" }, 484 | { name = "pydantic", marker = "sys_platform == 'linux'" }, 485 | { name = "pyyaml", marker = "sys_platform == 'linux'" }, 486 | { name = "requests", marker = "sys_platform == 'linux'" }, 487 | { name = "sqlparse", marker = "sys_platform == 'linux'" }, 488 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 489 | { name = "uvicorn", marker = "sys_platform == 'linux'" }, 490 | ] 491 | sdist = { url = "https://files.pythonhosted.org/packages/3e/62/5a229297385a906e20ffe7fec0e5c9e72a6f02bddebbab62fd6637e0bfaa/mlflow_skinny-2.21.3.tar.gz", hash = "sha256:4623144ad0a7441f13e0face059165754af72d4e16e4418abb16f56fcde50131", size = 5773036 } 492 | wheels = [ 493 | { url = "https://files.pythonhosted.org/packages/2d/f8/b71f88ca373f248fd7fdf3751f74c7b36a71b7ee2b5f4b803ee053ac963a/mlflow_skinny-2.21.3-py3-none-any.whl", hash = "sha256:a93d928a5123bfbfdcb8ded749156b99813158e8b16bdf137bebb0883cea353d", size = 6145084 }, 494 | ] 495 | 496 | [[package]] 497 | name = "msal" 498 | version = "1.32.0" 499 | source = { registry = "https://pypi.org/simple" } 500 | dependencies = [ 501 | { name = "cryptography", marker = "sys_platform == 'linux'" }, 502 | { name = "pyjwt", extra = ["crypto"], marker = "sys_platform == 'linux'" }, 503 | { name = "requests", marker = "sys_platform == 'linux'" }, 504 | ] 505 | sdist = { url = "https://files.pythonhosted.org/packages/aa/5f/ef42ef25fba682e83a8ee326a1a788e60c25affb58d014495349e37bce50/msal-1.32.0.tar.gz", hash = "sha256:5445fe3af1da6be484991a7ab32eaa82461dc2347de105b76af92c610c3335c2", size = 149817 } 506 | wheels = [ 507 | { url = "https://files.pythonhosted.org/packages/93/5a/2e663ef56a5d89eba962941b267ebe5be8c5ea340a9929d286e2f5fac505/msal-1.32.0-py3-none-any.whl", hash = "sha256:9dbac5384a10bbbf4dae5c7ea0d707d14e087b92c5aa4954b3feaa2d1aa0bcb7", size = 114655 }, 508 | ] 509 | 510 | [[package]] 511 | name = "msal-extensions" 512 | version = "1.3.1" 513 | source = { registry = "https://pypi.org/simple" } 514 | dependencies = [ 515 | { name = "msal", marker = "sys_platform == 'linux'" }, 516 | ] 517 | sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 } 518 | wheels = [ 519 | { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 }, 520 | ] 521 | 522 | [[package]] 523 | name = "msrest" 524 | version = "0.7.1" 525 | source = { registry = "https://pypi.org/simple" } 526 | dependencies = [ 527 | { name = "azure-core", marker = "sys_platform == 'linux'" }, 528 | { name = "certifi", marker = "sys_platform == 'linux'" }, 529 | { name = "isodate", marker = "sys_platform == 'linux'" }, 530 | { name = "requests", marker = "sys_platform == 'linux'" }, 531 | { name = "requests-oauthlib", marker = "sys_platform == 'linux'" }, 532 | ] 533 | sdist = { url = "https://files.pythonhosted.org/packages/68/77/8397c8fb8fc257d8ea0fa66f8068e073278c65f05acb17dcb22a02bfdc42/msrest-0.7.1.zip", hash = "sha256:6e7661f46f3afd88b75667b7187a92829924446c7ea1d169be8c4bb7eeb788b9", size = 175332 } 534 | wheels = [ 535 | { url = "https://files.pythonhosted.org/packages/15/cf/f2966a2638144491f8696c27320d5219f48a072715075d168b31d3237720/msrest-0.7.1-py3-none-any.whl", hash = "sha256:21120a810e1233e5e6cc7fe40b474eeb4ec6f757a15d7cf86702c369f9567c32", size = 85384 }, 536 | ] 537 | 538 | [[package]] 539 | name = "nodeenv" 540 | version = "1.9.1" 541 | source = { registry = "https://pypi.org/simple" } 542 | sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437 } 543 | wheels = [ 544 | { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314 }, 545 | ] 546 | 547 | [[package]] 548 | name = "oauthlib" 549 | version = "3.2.2" 550 | source = { registry = "https://pypi.org/simple" } 551 | sdist = { url = "https://files.pythonhosted.org/packages/6d/fa/fbf4001037904031639e6bfbfc02badfc7e12f137a8afa254df6c4c8a670/oauthlib-3.2.2.tar.gz", hash = "sha256:9859c40929662bec5d64f34d01c99e093149682a3f38915dc0655d5a633dd918", size = 177352 } 552 | wheels = [ 553 | { url = "https://files.pythonhosted.org/packages/7e/80/cab10959dc1faead58dc8384a781dfbf93cb4d33d50988f7a69f1b7c9bbe/oauthlib-3.2.2-py3-none-any.whl", hash = "sha256:8139f29aac13e25d502680e9e19963e83f16838d48a0d71c287fe40e7067fbca", size = 151688 }, 554 | ] 555 | 556 | [[package]] 557 | name = "opentelemetry-api" 558 | version = "1.32.1" 559 | source = { registry = "https://pypi.org/simple" } 560 | dependencies = [ 561 | { name = "deprecated", marker = "sys_platform == 'linux'" }, 562 | { name = "importlib-metadata", marker = "sys_platform == 'linux'" }, 563 | ] 564 | sdist = { url = "https://files.pythonhosted.org/packages/42/40/2359245cd33641c2736a0136a50813352d72f3fc209de28fb226950db4a1/opentelemetry_api-1.32.1.tar.gz", hash = "sha256:a5be71591694a4d9195caf6776b055aa702e964d961051a0715d05f8632c32fb", size = 64138 } 565 | wheels = [ 566 | { url = "https://files.pythonhosted.org/packages/12/f2/89ea3361a305466bc6460a532188830351220b5f0851a5fa133155c16eca/opentelemetry_api-1.32.1-py3-none-any.whl", hash = "sha256:bbd19f14ab9f15f0e85e43e6a958aa4cb1f36870ee62b7fd205783a112012724", size = 65287 }, 567 | ] 568 | 569 | [[package]] 570 | name = "opentelemetry-sdk" 571 | version = "1.32.1" 572 | source = { registry = "https://pypi.org/simple" } 573 | dependencies = [ 574 | { name = "opentelemetry-api", marker = "sys_platform == 'linux'" }, 575 | { name = "opentelemetry-semantic-conventions", marker = "sys_platform == 'linux'" }, 576 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 577 | ] 578 | sdist = { url = "https://files.pythonhosted.org/packages/a3/65/2069caef9257fae234ca0040d945c741aa7afbd83a7298ee70fc0bc6b6f4/opentelemetry_sdk-1.32.1.tar.gz", hash = "sha256:8ef373d490961848f525255a42b193430a0637e064dd132fd2a014d94792a092", size = 161044 } 579 | wheels = [ 580 | { url = "https://files.pythonhosted.org/packages/dc/00/d3976cdcb98027aaf16f1e980e54935eb820872792f0eaedd4fd7abb5964/opentelemetry_sdk-1.32.1-py3-none-any.whl", hash = "sha256:bba37b70a08038613247bc42beee5a81b0ddca422c7d7f1b097b32bf1c7e2f17", size = 118989 }, 581 | ] 582 | 583 | [[package]] 584 | name = "opentelemetry-semantic-conventions" 585 | version = "0.53b1" 586 | source = { registry = "https://pypi.org/simple" } 587 | dependencies = [ 588 | { name = "deprecated", marker = "sys_platform == 'linux'" }, 589 | { name = "opentelemetry-api", marker = "sys_platform == 'linux'" }, 590 | ] 591 | sdist = { url = "https://files.pythonhosted.org/packages/5e/b6/3c56e22e9b51bcb89edab30d54830958f049760bbd9ab0a759cece7bca88/opentelemetry_semantic_conventions-0.53b1.tar.gz", hash = "sha256:4c5a6fede9de61211b2e9fc1e02e8acacce882204cd770177342b6a3be682992", size = 114350 } 592 | wheels = [ 593 | { url = "https://files.pythonhosted.org/packages/27/6b/a8fb94760ef8da5ec283e488eb43235eac3ae7514385a51b6accf881e671/opentelemetry_semantic_conventions-0.53b1-py3-none-any.whl", hash = "sha256:21df3ed13f035f8f3ea42d07cbebae37020367a53b47f1ebee3b10a381a00208", size = 188443 }, 594 | ] 595 | 596 | [[package]] 597 | name = "packaging" 598 | version = "24.2" 599 | source = { registry = "https://pypi.org/simple" } 600 | sdist = { url = "https://files.pythonhosted.org/packages/d0/63/68dbb6eb2de9cb10ee4c9c14a0148804425e13c4fb20d61cce69f53106da/packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f", size = 163950 } 601 | wheels = [ 602 | { url = "https://files.pythonhosted.org/packages/88/ef/eb23f262cca3c0c4eb7ab1933c3b1f03d021f2c48f54763065b6f0e321be/packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759", size = 65451 }, 603 | ] 604 | 605 | [[package]] 606 | name = "platformdirs" 607 | version = "4.3.6" 608 | source = { registry = "https://pypi.org/simple" } 609 | sdist = { url = "https://files.pythonhosted.org/packages/13/fc/128cc9cb8f03208bdbf93d3aa862e16d376844a14f9a0ce5cf4507372de4/platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907", size = 21302 } 610 | wheels = [ 611 | { url = "https://files.pythonhosted.org/packages/3c/a6/bc1012356d8ece4d66dd75c4b9fc6c1f6650ddd5991e421177d9f8f671be/platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb", size = 18439 }, 612 | ] 613 | 614 | [[package]] 615 | name = "pluggy" 616 | version = "1.5.0" 617 | source = { registry = "https://pypi.org/simple" } 618 | sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955 } 619 | wheels = [ 620 | { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556 }, 621 | ] 622 | 623 | [[package]] 624 | name = "pre-commit" 625 | version = "4.2.0" 626 | source = { registry = "https://pypi.org/simple" } 627 | dependencies = [ 628 | { name = "cfgv", marker = "sys_platform == 'linux'" }, 629 | { name = "identify", marker = "sys_platform == 'linux'" }, 630 | { name = "nodeenv", marker = "sys_platform == 'linux'" }, 631 | { name = "pyyaml", marker = "sys_platform == 'linux'" }, 632 | { name = "virtualenv", marker = "sys_platform == 'linux'" }, 633 | ] 634 | sdist = { url = "https://files.pythonhosted.org/packages/08/39/679ca9b26c7bb2999ff122d50faa301e49af82ca9c066ec061cfbc0c6784/pre_commit-4.2.0.tar.gz", hash = "sha256:601283b9757afd87d40c4c4a9b2b5de9637a8ea02eaff7adc2d0fb4e04841146", size = 193424 } 635 | wheels = [ 636 | { url = "https://files.pythonhosted.org/packages/88/74/a88bf1b1efeae488a0c0b7bdf71429c313722d1fc0f377537fbe554e6180/pre_commit-4.2.0-py2.py3-none-any.whl", hash = "sha256:a009ca7205f1eb497d10b845e52c838a98b6cdd2102a6c8e4540e94ee75c58bd", size = 220707 }, 637 | ] 638 | 639 | [[package]] 640 | name = "protobuf" 641 | version = "5.29.4" 642 | source = { registry = "https://pypi.org/simple" } 643 | sdist = { url = "https://files.pythonhosted.org/packages/17/7d/b9dca7365f0e2c4fa7c193ff795427cfa6290147e5185ab11ece280a18e7/protobuf-5.29.4.tar.gz", hash = "sha256:4f1dfcd7997b31ef8f53ec82781ff434a28bf71d9102ddde14d076adcfc78c99", size = 424902 } 644 | wheels = [ 645 | { url = "https://files.pythonhosted.org/packages/a2/b5/bade14ae31ba871a139aa45e7a8183d869efe87c34a4850c87b936963261/protobuf-5.29.4-cp38-abi3-manylinux2014_aarch64.whl", hash = "sha256:aec4962f9ea93c431d5714ed1be1c93f13e1a8618e70035ba2b0564d9e633f2e", size = 319574 }, 646 | { url = "https://files.pythonhosted.org/packages/46/88/b01ed2291aae68b708f7d334288ad5fb3e7aa769a9c309c91a0d55cb91b0/protobuf-5.29.4-cp38-abi3-manylinux2014_x86_64.whl", hash = "sha256:d7d3f7d1d5a66ed4942d4fefb12ac4b14a29028b209d4bfb25c68ae172059922", size = 319672 }, 647 | { url = "https://files.pythonhosted.org/packages/12/fb/a586e0c973c95502e054ac5f81f88394f24ccc7982dac19c515acd9e2c93/protobuf-5.29.4-py3-none-any.whl", hash = "sha256:3fde11b505e1597f71b875ef2fc52062b6a9740e5f7c8997ce878b6009145862", size = 172551 }, 648 | ] 649 | 650 | [[package]] 651 | name = "pyasn1" 652 | version = "0.6.1" 653 | source = { registry = "https://pypi.org/simple" } 654 | sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 } 655 | wheels = [ 656 | { url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 }, 657 | ] 658 | 659 | [[package]] 660 | name = "pyasn1-modules" 661 | version = "0.4.2" 662 | source = { registry = "https://pypi.org/simple" } 663 | dependencies = [ 664 | { name = "pyasn1", marker = "sys_platform == 'linux'" }, 665 | ] 666 | sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 } 667 | wheels = [ 668 | { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 }, 669 | ] 670 | 671 | [[package]] 672 | name = "pycparser" 673 | version = "2.22" 674 | source = { registry = "https://pypi.org/simple" } 675 | sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 } 676 | wheels = [ 677 | { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 }, 678 | ] 679 | 680 | [[package]] 681 | name = "pydantic" 682 | version = "2.11.3" 683 | source = { registry = "https://pypi.org/simple" } 684 | dependencies = [ 685 | { name = "annotated-types", marker = "sys_platform == 'linux'" }, 686 | { name = "pydantic-core", marker = "sys_platform == 'linux'" }, 687 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 688 | { name = "typing-inspection", marker = "sys_platform == 'linux'" }, 689 | ] 690 | sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 } 691 | wheels = [ 692 | { url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 }, 693 | ] 694 | 695 | [[package]] 696 | name = "pydantic-core" 697 | version = "2.33.1" 698 | source = { registry = "https://pypi.org/simple" } 699 | dependencies = [ 700 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 701 | ] 702 | sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 } 703 | wheels = [ 704 | { url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 }, 705 | { url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 }, 706 | { url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 }, 707 | { url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 }, 708 | { url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 }, 709 | { url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 }, 710 | { url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 }, 711 | { url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 }, 712 | { url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 }, 713 | ] 714 | 715 | [[package]] 716 | name = "pyjwt" 717 | version = "2.10.1" 718 | source = { registry = "https://pypi.org/simple" } 719 | sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785 } 720 | wheels = [ 721 | { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997 }, 722 | ] 723 | 724 | [package.optional-dependencies] 725 | crypto = [ 726 | { name = "cryptography", marker = "sys_platform == 'linux'" }, 727 | ] 728 | 729 | [[package]] 730 | name = "pyright" 731 | version = "1.1.396" 732 | source = { registry = "https://pypi.org/simple" } 733 | dependencies = [ 734 | { name = "nodeenv", marker = "sys_platform == 'linux'" }, 735 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 736 | ] 737 | sdist = { url = "https://files.pythonhosted.org/packages/bd/73/f20cb1dea1bdc1774e7f860fb69dc0718c7d8dea854a345faec845eb086a/pyright-1.1.396.tar.gz", hash = "sha256:142901f5908f5a0895be3d3befcc18bedcdb8cc1798deecaec86ef7233a29b03", size = 3814400 } 738 | wheels = [ 739 | { url = "https://files.pythonhosted.org/packages/80/be/ecb7cfb42d242b7ee764b52e6ff4782beeec00e3b943a3ec832b281f9da6/pyright-1.1.396-py3-none-any.whl", hash = "sha256:c635e473095b9138c471abccca22b9fedbe63858e0b40d4fc4b67da041891844", size = 5689355 }, 740 | ] 741 | 742 | [[package]] 743 | name = "pytest" 744 | version = "8.3.5" 745 | source = { registry = "https://pypi.org/simple" } 746 | dependencies = [ 747 | { name = "iniconfig", marker = "sys_platform == 'linux'" }, 748 | { name = "packaging", marker = "sys_platform == 'linux'" }, 749 | { name = "pluggy", marker = "sys_platform == 'linux'" }, 750 | ] 751 | sdist = { url = "https://files.pythonhosted.org/packages/ae/3c/c9d525a414d506893f0cd8a8d0de7706446213181570cdbd766691164e40/pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845", size = 1450891 } 752 | wheels = [ 753 | { url = "https://files.pythonhosted.org/packages/30/3d/64ad57c803f1fa1e963a7946b6e0fea4a70df53c1a7fed304586539c2bac/pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820", size = 343634 }, 754 | ] 755 | 756 | [[package]] 757 | name = "pytest-cov" 758 | version = "6.1.1" 759 | source = { registry = "https://pypi.org/simple" } 760 | dependencies = [ 761 | { name = "coverage", marker = "sys_platform == 'linux'" }, 762 | { name = "pytest", marker = "sys_platform == 'linux'" }, 763 | ] 764 | sdist = { url = "https://files.pythonhosted.org/packages/25/69/5f1e57f6c5a39f81411b550027bf72842c4567ff5fd572bed1edc9e4b5d9/pytest_cov-6.1.1.tar.gz", hash = "sha256:46935f7aaefba760e716c2ebfbe1c216240b9592966e7da99ea8292d4d3e2a0a", size = 66857 } 765 | wheels = [ 766 | { url = "https://files.pythonhosted.org/packages/28/d0/def53b4a790cfb21483016430ed828f64830dd981ebe1089971cd10cab25/pytest_cov-6.1.1-py3-none-any.whl", hash = "sha256:bddf29ed2d0ab6f4df17b4c55b0a657287db8684af9c42ea546b21b1041b3dde", size = 23841 }, 767 | ] 768 | 769 | [[package]] 770 | name = "python-dateutil" 771 | version = "2.9.0.post0" 772 | source = { registry = "https://pypi.org/simple" } 773 | dependencies = [ 774 | { name = "six", marker = "sys_platform == 'linux'" }, 775 | ] 776 | sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432 } 777 | wheels = [ 778 | { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892 }, 779 | ] 780 | 781 | [[package]] 782 | name = "pytz" 783 | version = "2025.2" 784 | source = { registry = "https://pypi.org/simple" } 785 | sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884 } 786 | wheels = [ 787 | { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225 }, 788 | ] 789 | 790 | [[package]] 791 | name = "pyyaml" 792 | version = "6.0.2" 793 | source = { registry = "https://pypi.org/simple" } 794 | sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631 } 795 | wheels = [ 796 | { 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 }, 797 | { 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 }, 798 | { 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 }, 799 | { 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 }, 800 | { 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 }, 801 | ] 802 | 803 | [[package]] 804 | name = "requests" 805 | version = "2.32.3" 806 | source = { registry = "https://pypi.org/simple" } 807 | dependencies = [ 808 | { name = "certifi", marker = "sys_platform == 'linux'" }, 809 | { name = "charset-normalizer", marker = "sys_platform == 'linux'" }, 810 | { name = "idna", marker = "sys_platform == 'linux'" }, 811 | { name = "urllib3", marker = "sys_platform == 'linux'" }, 812 | ] 813 | sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 } 814 | wheels = [ 815 | { url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 }, 816 | ] 817 | 818 | [[package]] 819 | name = "requests-oauthlib" 820 | version = "2.0.0" 821 | source = { registry = "https://pypi.org/simple" } 822 | dependencies = [ 823 | { name = "oauthlib", marker = "sys_platform == 'linux'" }, 824 | { name = "requests", marker = "sys_platform == 'linux'" }, 825 | ] 826 | sdist = { url = "https://files.pythonhosted.org/packages/42/f2/05f29bc3913aea15eb670be136045bf5c5bbf4b99ecb839da9b422bb2c85/requests-oauthlib-2.0.0.tar.gz", hash = "sha256:b3dffaebd884d8cd778494369603a9e7b58d29111bf6b41bdc2dcd87203af4e9", size = 55650 } 827 | wheels = [ 828 | { url = "https://files.pythonhosted.org/packages/3b/5d/63d4ae3b9daea098d5d6f5da83984853c1bbacd5dc826764b249fe119d24/requests_oauthlib-2.0.0-py2.py3-none-any.whl", hash = "sha256:7dd8a5c40426b779b0868c404bdef9768deccf22749cde15852df527e6269b36", size = 24179 }, 829 | ] 830 | 831 | [[package]] 832 | name = "rsa" 833 | version = "4.9" 834 | source = { registry = "https://pypi.org/simple" } 835 | dependencies = [ 836 | { name = "pyasn1", marker = "sys_platform == 'linux'" }, 837 | ] 838 | sdist = { url = "https://files.pythonhosted.org/packages/aa/65/7d973b89c4d2351d7fb232c2e452547ddfa243e93131e7cfa766da627b52/rsa-4.9.tar.gz", hash = "sha256:e38464a49c6c85d7f1351b0126661487a7e0a14a50f1675ec50eb34d4f20ef21", size = 29711 } 839 | wheels = [ 840 | { url = "https://files.pythonhosted.org/packages/49/97/fa78e3d2f65c02c8e1268b9aba606569fe97f6c8f7c2d74394553347c145/rsa-4.9-py3-none-any.whl", hash = "sha256:90260d9058e514786967344d0ef75fa8727eed8a7d2e43ce9f4bcf1b536174f7", size = 34315 }, 841 | ] 842 | 843 | [[package]] 844 | name = "ruff" 845 | version = "0.11.5" 846 | source = { registry = "https://pypi.org/simple" } 847 | sdist = { url = "https://files.pythonhosted.org/packages/45/71/5759b2a6b2279bb77fe15b1435b89473631c2cd6374d45ccdb6b785810be/ruff-0.11.5.tar.gz", hash = "sha256:cae2e2439cb88853e421901ec040a758960b576126dab520fa08e9de431d1bef", size = 3976488 } 848 | wheels = [ 849 | { url = "https://files.pythonhosted.org/packages/23/db/6efda6381778eec7f35875b5cbefd194904832a1153d68d36d6b269d81a8/ruff-0.11.5-py3-none-linux_armv6l.whl", hash = "sha256:2561294e108eb648e50f210671cc56aee590fb6167b594144401532138c66c7b", size = 10103150 }, 850 | { url = "https://files.pythonhosted.org/packages/b8/ca/b9bf954cfed165e1a0c24b86305d5c8ea75def256707f2448439ac5e0d8b/ruff-0.11.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0947c0a1afa75dcb5db4b34b070ec2bccee869d40e6cc8ab25aca11a7d527794", size = 10415338 }, 851 | { url = "https://files.pythonhosted.org/packages/d9/4d/2522dde4e790f1b59885283f8786ab0046958dfd39959c81acc75d347467/ruff-0.11.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ad871ff74b5ec9caa66cb725b85d4ef89b53f8170f47c3406e32ef040400b038", size = 9965277 }, 852 | { url = "https://files.pythonhosted.org/packages/e5/7a/749f56f150eef71ce2f626a2f6988446c620af2f9ba2a7804295ca450397/ruff-0.11.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6cf918390cfe46d240732d4d72fa6e18e528ca1f60e318a10835cf2fa3dc19f", size = 11541614 }, 853 | { url = "https://files.pythonhosted.org/packages/89/b2/7d9b8435222485b6aac627d9c29793ba89be40b5de11584ca604b829e960/ruff-0.11.5-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:56145ee1478582f61c08f21076dc59153310d606ad663acc00ea3ab5b2125f82", size = 12198873 }, 854 | { url = "https://files.pythonhosted.org/packages/00/e0/a1a69ef5ffb5c5f9c31554b27e030a9c468fc6f57055886d27d316dfbabd/ruff-0.11.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e5f66f8f1e8c9fc594cbd66fbc5f246a8d91f916cb9667e80208663ec3728304", size = 11670190 }, 855 | { url = "https://files.pythonhosted.org/packages/05/61/c1c16df6e92975072c07f8b20dad35cd858e8462b8865bc856fe5d6ccb63/ruff-0.11.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:80b4df4d335a80315ab9afc81ed1cff62be112bd165e162b5eed8ac55bfc8470", size = 13902301 }, 856 | { url = "https://files.pythonhosted.org/packages/79/89/0af10c8af4363304fd8cb833bd407a2850c760b71edf742c18d5a87bb3ad/ruff-0.11.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3068befab73620b8a0cc2431bd46b3cd619bc17d6f7695a3e1bb166b652c382a", size = 11350132 }, 857 | { url = "https://files.pythonhosted.org/packages/b9/e1/ecb4c687cbf15164dd00e38cf62cbab238cad05dd8b6b0fc68b0c2785e15/ruff-0.11.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f5da2e710a9641828e09aa98b92c9ebbc60518fdf3921241326ca3e8f8e55b8b", size = 10312937 }, 858 | { url = "https://files.pythonhosted.org/packages/cf/4f/0e53fe5e500b65934500949361e3cd290c5ba60f0324ed59d15f46479c06/ruff-0.11.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ef39f19cb8ec98cbc762344921e216f3857a06c47412030374fffd413fb8fd3a", size = 9936683 }, 859 | { url = "https://files.pythonhosted.org/packages/04/a8/8183c4da6d35794ae7f76f96261ef5960853cd3f899c2671961f97a27d8e/ruff-0.11.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b2a7cedf47244f431fd11aa5a7e2806dda2e0c365873bda7834e8f7d785ae159", size = 10950217 }, 860 | { url = "https://files.pythonhosted.org/packages/26/88/9b85a5a8af21e46a0639b107fcf9bfc31da4f1d263f2fc7fbe7199b47f0a/ruff-0.11.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:81be52e7519f3d1a0beadcf8e974715b2dfc808ae8ec729ecfc79bddf8dbb783", size = 11404521 }, 861 | ] 862 | 863 | [[package]] 864 | name = "scaffolding" 865 | version = "0.2.0" 866 | source = { virtual = "." } 867 | dependencies = [ 868 | { name = "example", marker = "sys_platform == 'linux'" }, 869 | { name = "example-reader-step", marker = "sys_platform == 'linux'" }, 870 | { name = "example-writer-step", marker = "sys_platform == 'linux'" }, 871 | { name = "shared", marker = "sys_platform == 'linux'" }, 872 | ] 873 | 874 | [package.dev-dependencies] 875 | dev = [ 876 | { name = "pre-commit", marker = "sys_platform == 'linux'" }, 877 | { name = "pyright", marker = "sys_platform == 'linux'" }, 878 | { name = "pytest", marker = "sys_platform == 'linux'" }, 879 | { name = "pytest-cov", marker = "sys_platform == 'linux'" }, 880 | { name = "ruff", marker = "sys_platform == 'linux'" }, 881 | ] 882 | 883 | [package.metadata] 884 | requires-dist = [ 885 | { name = "example", editable = "packages/example" }, 886 | { name = "example-reader-step", editable = "packages/example-reader-step" }, 887 | { name = "example-writer-step", editable = "packages/example-writer-step" }, 888 | { name = "shared", editable = "packages/shared" }, 889 | ] 890 | 891 | [package.metadata.requires-dev] 892 | dev = [ 893 | { name = "pre-commit", specifier = "==4.2.*" }, 894 | { name = "pyright", specifier = "==1.1.*" }, 895 | { name = "pytest", specifier = "==8.3.*" }, 896 | { name = "pytest-cov", specifier = "==6.1.*" }, 897 | { name = "ruff", specifier = "==0.11.*" }, 898 | ] 899 | 900 | [[package]] 901 | name = "shared" 902 | version = "0.1.0" 903 | source = { editable = "packages/shared" } 904 | 905 | [[package]] 906 | name = "six" 907 | version = "1.17.0" 908 | source = { registry = "https://pypi.org/simple" } 909 | sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031 } 910 | wheels = [ 911 | { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050 }, 912 | ] 913 | 914 | [[package]] 915 | name = "smmap" 916 | version = "5.0.2" 917 | source = { registry = "https://pypi.org/simple" } 918 | sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329 } 919 | wheels = [ 920 | { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303 }, 921 | ] 922 | 923 | [[package]] 924 | name = "sniffio" 925 | version = "1.3.1" 926 | source = { registry = "https://pypi.org/simple" } 927 | sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } 928 | wheels = [ 929 | { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, 930 | ] 931 | 932 | [[package]] 933 | name = "sqlparse" 934 | version = "0.5.3" 935 | source = { registry = "https://pypi.org/simple" } 936 | sdist = { url = "https://files.pythonhosted.org/packages/e5/40/edede8dd6977b0d3da179a342c198ed100dd2aba4be081861ee5911e4da4/sqlparse-0.5.3.tar.gz", hash = "sha256:09f67787f56a0b16ecdbde1bfc7f5d9c3371ca683cfeaa8e6ff60b4807ec9272", size = 84999 } 937 | wheels = [ 938 | { url = "https://files.pythonhosted.org/packages/a9/5c/bfd6bd0bf979426d405cc6e71eceb8701b148b16c21d2dc3c261efc61c7b/sqlparse-0.5.3-py3-none-any.whl", hash = "sha256:cf2196ed3418f3ba5de6af7e82c694a9fbdbfecccdfc72e281548517081f16ca", size = 44415 }, 939 | ] 940 | 941 | [[package]] 942 | name = "starlette" 943 | version = "0.46.2" 944 | source = { registry = "https://pypi.org/simple" } 945 | dependencies = [ 946 | { name = "anyio", marker = "sys_platform == 'linux'" }, 947 | ] 948 | sdist = { url = "https://files.pythonhosted.org/packages/ce/20/08dfcd9c983f6a6f4a1000d934b9e6d626cff8d2eeb77a89a68eef20a2b7/starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5", size = 2580846 } 949 | wheels = [ 950 | { url = "https://files.pythonhosted.org/packages/8b/0c/9d30a4ebeb6db2b25a841afbb80f6ef9a854fc3b41be131d249a977b4959/starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35", size = 72037 }, 951 | ] 952 | 953 | [[package]] 954 | name = "typing-extensions" 955 | version = "4.12.2" 956 | source = { registry = "https://pypi.org/simple" } 957 | sdist = { url = "https://files.pythonhosted.org/packages/df/db/f35a00659bc03fec321ba8bce9420de607a1d37f8342eee1863174c69557/typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8", size = 85321 } 958 | wheels = [ 959 | { url = "https://files.pythonhosted.org/packages/26/9f/ad63fc0248c5379346306f8668cda6e2e2e9c95e01216d2b8ffd9ff037d0/typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d", size = 37438 }, 960 | ] 961 | 962 | [[package]] 963 | name = "typing-inspection" 964 | version = "0.4.0" 965 | source = { registry = "https://pypi.org/simple" } 966 | dependencies = [ 967 | { name = "typing-extensions", marker = "sys_platform == 'linux'" }, 968 | ] 969 | sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 } 970 | wheels = [ 971 | { url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 }, 972 | ] 973 | 974 | [[package]] 975 | name = "urllib3" 976 | version = "2.4.0" 977 | source = { registry = "https://pypi.org/simple" } 978 | sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672 } 979 | wheels = [ 980 | { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680 }, 981 | ] 982 | 983 | [[package]] 984 | name = "uvicorn" 985 | version = "0.34.1" 986 | source = { registry = "https://pypi.org/simple" } 987 | dependencies = [ 988 | { name = "click", marker = "sys_platform == 'linux'" }, 989 | { name = "h11", marker = "sys_platform == 'linux'" }, 990 | ] 991 | sdist = { url = "https://files.pythonhosted.org/packages/86/37/dd92f1f9cedb5eaf74d9999044306e06abe65344ff197864175dbbd91871/uvicorn-0.34.1.tar.gz", hash = "sha256:af981725fc4b7ffc5cb3b0e9eda6258a90c4b52cb2a83ce567ae0a7ae1757afc", size = 76755 } 992 | wheels = [ 993 | { url = "https://files.pythonhosted.org/packages/5f/38/a5801450940a858c102a7ad9e6150146a25406a119851c993148d56ab041/uvicorn-0.34.1-py3-none-any.whl", hash = "sha256:984c3a8c7ca18ebaad15995ee7401179212c59521e67bfc390c07fa2b8d2e065", size = 62404 }, 994 | ] 995 | 996 | [[package]] 997 | name = "virtualenv" 998 | version = "20.29.3" 999 | source = { registry = "https://pypi.org/simple" } 1000 | dependencies = [ 1001 | { name = "distlib", marker = "sys_platform == 'linux'" }, 1002 | { name = "filelock", marker = "sys_platform == 'linux'" }, 1003 | { name = "platformdirs", marker = "sys_platform == 'linux'" }, 1004 | ] 1005 | sdist = { url = "https://files.pythonhosted.org/packages/c7/9c/57d19fa093bcf5ac61a48087dd44d00655f85421d1aa9722f8befbf3f40a/virtualenv-20.29.3.tar.gz", hash = "sha256:95e39403fcf3940ac45bc717597dba16110b74506131845d9b687d5e73d947ac", size = 4320280 } 1006 | wheels = [ 1007 | { url = "https://files.pythonhosted.org/packages/c2/eb/c6db6e3001d58c6a9e67c74bb7b4206767caa3ccc28c6b9eaf4c23fb4e34/virtualenv-20.29.3-py3-none-any.whl", hash = "sha256:3e3d00f5807e83b234dfb6122bf37cfadf4be216c53a49ac059d02414f819170", size = 4301458 }, 1008 | ] 1009 | 1010 | [[package]] 1011 | name = "wrapt" 1012 | version = "1.17.2" 1013 | source = { registry = "https://pypi.org/simple" } 1014 | sdist = { url = "https://files.pythonhosted.org/packages/c3/fc/e91cc220803d7bc4db93fb02facd8461c37364151b8494762cc88b0fbcef/wrapt-1.17.2.tar.gz", hash = "sha256:41388e9d4d1522446fe79d3213196bd9e3b301a336965b9e27ca2788ebd122f3", size = 55531 } 1015 | wheels = [ 1016 | { url = "https://files.pythonhosted.org/packages/73/54/3bfe5a1febbbccb7a2f77de47b989c0b85ed3a6a41614b104204a788c20e/wrapt-1.17.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5bb1d0dbf99411f3d871deb6faa9aabb9d4e744d67dcaaa05399af89d847a91d", size = 88721 }, 1017 | { url = "https://files.pythonhosted.org/packages/25/cb/7262bc1b0300b4b64af50c2720ef958c2c1917525238d661c3e9a2b71b7b/wrapt-1.17.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d18a4865f46b8579d44e4fe1e2bcbc6472ad83d98e22a26c963d46e4c125ef0b", size = 80899 }, 1018 | { url = "https://files.pythonhosted.org/packages/2a/5a/04cde32b07a7431d4ed0553a76fdb7a61270e78c5fd5a603e190ac389f14/wrapt-1.17.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc570b5f14a79734437cb7b0500376b6b791153314986074486e0b0fa8d71d98", size = 89222 }, 1019 | { url = "https://files.pythonhosted.org/packages/09/28/2e45a4f4771fcfb109e244d5dbe54259e970362a311b67a965555ba65026/wrapt-1.17.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6d9187b01bebc3875bac9b087948a2bccefe464a7d8f627cf6e48b1bbae30f82", size = 86707 }, 1020 | { url = "https://files.pythonhosted.org/packages/c6/d2/dcb56bf5f32fcd4bd9aacc77b50a539abdd5b6536872413fd3f428b21bed/wrapt-1.17.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9e8659775f1adf02eb1e6f109751268e493c73716ca5761f8acb695e52a756ae", size = 79685 }, 1021 | { url = "https://files.pythonhosted.org/packages/80/4e/eb8b353e36711347893f502ce91c770b0b0929f8f0bed2670a6856e667a9/wrapt-1.17.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e8b2816ebef96d83657b56306152a93909a83f23994f4b30ad4573b00bd11bb9", size = 87567 }, 1022 | { url = "https://files.pythonhosted.org/packages/2d/82/f56956041adef78f849db6b289b282e72b55ab8045a75abad81898c28d19/wrapt-1.17.2-py3-none-any.whl", hash = "sha256:b18f2d1533a71f069c7f82d524a52599053d4c7166e9dd374ae2136b7f40f7c8", size = 23594 }, 1023 | ] 1024 | 1025 | [[package]] 1026 | name = "zipp" 1027 | version = "3.21.0" 1028 | source = { registry = "https://pypi.org/simple" } 1029 | sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545 } 1030 | wheels = [ 1031 | { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630 }, 1032 | ] 1033 | --------------------------------------------------------------------------------