diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md new file mode 100644 index 0000000..77b2797 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -0,0 +1,38 @@ +--- +name: Bug Report +about: Incorrect or unexpected behavior +title: "[BUG]" +labels: bug +assignees: '' + +--- + +**To Reproduce** +Steps to reproduce the behavior: + +1. Go to '...' +2. Click on '....' +3. Scroll down to '....' +4. See error + +**Expected behavior** +A clear and concise description of what you expected to happen. + +**Screenshots** +If applicable, add screenshots to help explain your problem. + +**Desktop (please complete the following information if applicable):** + +- OS: [e.g. iOS] +- Browser: [e.g. chrome, safari] +- Version: [e.g. 22] + +**Smartphone (please complete the following information if applicable):** + +- Device: [e.g. iPhone6] +- OS: [e.g. iOS8.1] +- Browser: [e.g. stock browser, safari] +- Version: [e.g. 22] + +**Additional context** +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature-request.md b/.github/ISSUE_TEMPLATE/feature-request.md new file mode 100644 index 0000000..77315b6 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature-request.md @@ -0,0 +1,20 @@ +--- +name: Feature Request +about: Functionality you want that is not yet supported +title: "[FEATURE REQUEST]" +labels: enhancement +assignees: '' + +--- + +**Is your feature request related to a problem? Please describe.** +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +**Describe the solution you'd like** +A clear and concise description of what you want to happen. + +**Describe alternatives you've considered** +A clear and concise description of any alternative solutions or features you've considered. + +**Additional context** +Add any other context or screenshots about the feature request here. diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..8d3ed9a --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,34 @@ +name: lint +run-name: ${{ github.actor }} is linting the package + +on: + push: + branches: + - dev + pull_request: + branches: + - dev + +jobs: + tests: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set Up Python Environment + uses: actions/setup-python@v4 + with: + python-version: "3.11" + + - name: Install Poetry + uses: snok/install-poetry@v1.3.1 + + - name: Install dependencies + run: poetry install --with dev + + - name: Run Ruff + run: poetry run ruff check . + + - name: Run MyPy + run: poetry run mypy . diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..68ff58c --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,34 @@ +name: release +run-name: ${{ github.actor }} is uploading a new release to PyPI + +on: + release: + types: [published] + +permissions: + contents: read + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v3 + + - name: Set Up Python 3.10 + uses: actions/setup-python@v4 + with: + python-version: "3.10" + + - name: Install Poetry + uses: snok/install-poetry@v1.3.1 + env: + ACTIONS_ALLOW_UNSECURE_COMMANDS: "true" + + - name: Get release version + run: echo "RELEASE_VERSION=$(poetry version | awk '{print $2}')" >> $GITHUB_ENV + + - name: Build And Publish Python Package + run: poetry publish --build + env: + POETRY_PYPI_TOKEN_PYPI: ${{ secrets.PYPI_TOKEN }} diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..e75f8f9 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,47 @@ +name: tests +run-name: ${{ github.actor }} is testing the package + +on: + push: + branches: + - dev + pull_request: + branches: + - dev + +jobs: + tests: + runs-on: ubuntu-latest + + strategy: + matrix: + python-version: ["3.9", "3.10", "3.11", "3.12"] + + steps: + - uses: actions/checkout@v3 + + - name: Set Up Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Poetry + uses: snok/install-poetry@v1.3.1 + + - name: Install dependencies + run: poetry install --with dev + + - name: Run Tests + run: poetry run pytest tests/ --cov=./ --cov-report=xml + + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@v3 + with: + directory: ./ + env_vars: OS,PYTHON + fail_ci_if_error: true + files: ./coverage.xml,!./cache + flags: tests + name: codecov-umbrella + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true diff --git a/.gitignore b/.gitignore index 68bc17f..4d1cc59 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Local files +.DS_Store + # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] diff --git a/.vscode/extensions.json b/.vscode/extensions.json new file mode 100644 index 0000000..8514cbd --- /dev/null +++ b/.vscode/extensions.json @@ -0,0 +1,16 @@ +{ + "recommendations": [ + "charliermarsh.ruff", + "DavidAnson.vscode-markdownlint", + "eamodio.gitlens", + "github.vscode-github-actions", + "ms-toolsai.jupyter", + "ms-toolsai.jupyter-keymap", + "ms-toolsai.jupyter-renderers", + "ms-python.mypy", + "ms-python.python", + "ms-python.vscode-pylance", + "ms-toolsai.vscode-jupyter-cell-tags", + "ms-toolsai.vscode-jupyter-slideshow" + ] +} diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 0000000..a1df80d --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,28 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Pytest", + "type": "debugpy", + "request": "launch", + "module": "pytest", + "args": [ + "-vv", + "-s", + "${file}" // Runs tests in the current file, you can customize this + ], + "console": "integratedTerminal", + "justMyCode": false + }, + { + "name": "Python Debugger: Current File", + "type": "debugpy", + "request": "launch", + "program": "${file}", + "console": "integratedTerminal" + } + ] +} diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..63c7df1 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,27 @@ +{ + "files.autoSave": "onFocusChange", + "editor.formatOnSave": true, + "editor.formatOnSaveMode": "file", + "files.insertFinalNewline": true, + "python.testing.unittestEnabled": false, + "python.testing.pytestEnabled": true, + "[python]": { + "editor.rulers": [88], + "editor.tabSize": 4, + "editor.codeActionsOnSave": { + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "[json]": { + "editor.tabSize": 2, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[yml]": { + "editor.tabSize": 2, + "editor.defaultFormatter": "esbenp.prettier-vscode" + }, + "[markdown]": { + "editor.formatOnSave": false + } +} diff --git a/README.md b/docs/README.md similarity index 100% rename from README.md rename to docs/README.md diff --git a/mirascope_cli/__init__.py b/mirascope_cli/__init__.py new file mode 100644 index 0000000..93c7693 --- /dev/null +++ b/mirascope_cli/__init__.py @@ -0,0 +1,3 @@ +"""This module contains all functionality related to the Mirascope CLI.""" + +from .main import app diff --git a/mirascope_cli/commands/__init__.py b/mirascope_cli/commands/__init__.py new file mode 100644 index 0000000..d22a113 --- /dev/null +++ b/mirascope_cli/commands/__init__.py @@ -0,0 +1,6 @@ +"""The Mirascope CLI commands module""" +from .add import add_command +from .init import init_command +from .remove import remove_command +from .status import status_command +from .use import use_command diff --git a/mirascope_cli/commands/add.py b/mirascope_cli/commands/add.py new file mode 100644 index 0000000..24141d1 --- /dev/null +++ b/mirascope_cli/commands/add.py @@ -0,0 +1,113 @@ +"""The add command for the Mirascope CLI.""" +import os + +from typer import Argument + +from ..constants import CURRENT_REVISION_KEY, LATEST_REVISION_KEY +from ..enums import MirascopeCommand +from ..schemas import MirascopeCliVariables +from ..utils import ( + check_status, + get_prompt_versions, + get_user_mirascope_settings, + parse_prompt_file_name, + prompts_directory_files, + run_format_command, + update_version_text_file, + write_prompt_to_template, +) + + +def add_command( + prompt_file_name: str = Argument( + help="Prompt file to add", + autocompletion=prompts_directory_files, + parser=parse_prompt_file_name, + default="", + ), +): + """Adds the given prompt to the specified version directory. + + The contents of the prompt in the user's prompts directory are copied to the version + directory with the next revision number, and the version file is updated with the + new revision. + + Args: + prompt_file_name: The name of the prompt file to add. + + Raises: + FileNotFoundError: If the file is not found in the specified prompts directory. + """ + mirascope_settings = get_user_mirascope_settings() + version_directory_path = mirascope_settings.versions_location + prompt_directory_path = mirascope_settings.prompts_location + version_file_name = mirascope_settings.version_file_name + auto_tag = mirascope_settings.auto_tag + # Check status before continuing + used_prompt_path = check_status(mirascope_settings, prompt_file_name) + if not used_prompt_path: + print("No changes detected.") + return + prompt_versions_directory = os.path.join(version_directory_path, prompt_file_name) + + # Check if prompt file exists + if not os.path.exists(f"{prompt_directory_path}/{prompt_file_name}.py"): + raise FileNotFoundError( + f"Prompt {prompt_file_name}.py not found in {prompt_directory_path}" + ) + # Create prompt versions directory if it doesn't exist + if not os.path.exists(prompt_versions_directory): + os.makedirs(prompt_versions_directory) + version_file_path = os.path.join(prompt_versions_directory, version_file_name) + versions = get_prompt_versions(version_file_path) + + # Open user's prompt file + user_prompt_file = os.path.join(prompt_directory_path, f"{prompt_file_name}.py") + with open(user_prompt_file, "r+", encoding="utf-8") as file: + # Increment revision id + if versions.latest_revision is None: + # first revision + revision_id = "0001" + else: + # default branch with incrementation + latest_revision_id = versions.latest_revision + revision_id = f"{int(latest_revision_id)+1:04}" + # Create revision file + revision_file = os.path.join( + prompt_versions_directory, f"{revision_id}_{prompt_file_name}.py" + ) + custom_variables = MirascopeCliVariables( + prev_revision_id=versions.current_revision, + revision_id=revision_id, + ) + prompt_file = file.read() + + if auto_tag: + new_prompt_file = write_prompt_to_template( + prompt_file, MirascopeCommand.USE, custom_variables + ) + # Replace contents of user's prompt file with new prompt file with tags + file.seek(0) + file.write(new_prompt_file) + file.truncate() + # Reset file pointer to beginning of file for revision file read + file.seek(0) + run_format_command(user_prompt_file) + with open( + revision_file, + "w+", + encoding="utf-8", + ) as file2: + file2.write( + write_prompt_to_template( + prompt_file, MirascopeCommand.ADD, custom_variables + ) + ) + keys_to_update = { + CURRENT_REVISION_KEY: revision_id, + LATEST_REVISION_KEY: revision_id, + } + update_version_text_file(version_file_path, keys_to_update) + if revision_file: + run_format_command(revision_file) + print("Adding " f"{prompt_versions_directory}/{revision_id}_{prompt_file_name}.py") diff --git a/mirascope_cli/commands/init.py b/mirascope_cli/commands/init.py new file mode 100644 index 0000000..3db8e98 --- /dev/null +++ b/mirascope_cli/commands/init.py @@ -0,0 +1,80 @@ +"""The init command for the Mirascope CLI.""" +import os +from importlib.resources import files +from pathlib import Path + +from jinja2 import Template +from typer import Option + +from ..schemas import MirascopeSettings + + +def init_command( + mirascope_location: str = Option( + help="Main mirascope directory", default=".mirascope" + ), + prompts_location: str = Option( + help="Location of prompts directory", default="prompts" + ), +) -> None: + """Initializes the mirascope project. + + Creates the project structure and files needed for mirascope to work. + + Initial project structure: + ``` + | + |-- mirascope.ini + |-- .mirascope + | |-- prompt_template.j2 + | |-- versions/ + | | |-- / + | | | |-- version.txt + | | | |-- _.py + |-- prompts/ + ``` + + Args: + mirascope_location: The root mirascope directory to create. + prompts_location: The user's prompts directory. + """ + destination_dir = Path.cwd() + versions_directory = os.path.join(mirascope_location, "versions") + os.makedirs(versions_directory, exist_ok=True) + print(f"Creating {destination_dir}/{versions_directory}") + os.makedirs(prompts_location, exist_ok=True) + print(f"Creating {destination_dir}/{prompts_location}") + prompts_init_file: Path = Path(f"{destination_dir}/{prompts_location}/__init__.py") + if not prompts_init_file.is_file(): + prompts_init_file.touch() + print(f"Creating {prompts_init_file}") + # Create the 'mirascope.ini' file in the current directory with some default values + ini_settings = MirascopeSettings( + mirascope_location=mirascope_location, + versions_location="versions", + prompts_location=prompts_location, + version_file_name="version.txt", + auto_tag=True, + ) + + # Get templates from the mirascope_cli.generic package + generic_file_path = files("mirascope_cli.generic") + ini_path = generic_file_path.joinpath("mirascope.ini.j2") + with open(str(ini_path), "r", encoding="utf-8") as file: + template = Template(file.read()) + rendered_content = template.render(ini_settings.model_dump()) + destination_file_path = destination_dir / "mirascope.ini" + with open(destination_file_path, "w", encoding="utf-8") as destination_file: + destination_file.write(rendered_content) + print(f"Creating {destination_file_path}") + + # Create the 'prompt_template.j2' file in the mirascope directory specified by user + prompt_template_path = generic_file_path.joinpath("prompt_template.j2") + with open(str(prompt_template_path), "r", encoding="utf-8") as file: + content = file.read() + template_path = os.path.join(mirascope_location, "prompt_template.j2") + with open(template_path, "w", encoding="utf-8") as file: + file.write(content) + print(f"Creating {destination_dir}/{template_path}") + + print("Initialization complete.") diff --git a/mirascope_cli/commands/py.typed b/mirascope_cli/commands/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/mirascope_cli/commands/remove.py b/mirascope_cli/commands/remove.py new file mode 100644 index 0000000..d2f83cd --- /dev/null +++ b/mirascope_cli/commands/remove.py @@ -0,0 +1,85 @@ +"""The remove command for the Mirascope CLI.""" +import os + +from typer import Argument + +from ..utils import ( + find_prompt_path, + find_prompt_paths, + get_prompt_versions, + get_user_mirascope_settings, + parse_prompt_file_name, + prompts_directory_files, +) + + +def remove_command( + prompt_file_name: str = Argument( + help="Prompt file to remove", + autocompletion=prompts_directory_files, + parser=parse_prompt_file_name, + default="", + ), + version: str = Argument( + help="Version of prompt to use", + ), +): + """Removes the version from the versions directory + + All versions with prev_revision_id matching the deleted version are detached. + + Args: + prompt_file_name: The name of the prompt file to remove. + version: The version of the prompt to remove + + Raises: + FileNotFoundError: If the file is not found in the specified prompts or + versions directory. + """ + mirascope_settings = get_user_mirascope_settings() + version_directory_path = mirascope_settings.versions_location + version_file_name = mirascope_settings.version_file_name + prompt_versions_directory = os.path.join(version_directory_path, prompt_file_name) + version_file_path = os.path.join(prompt_versions_directory, version_file_name) + + revisions = get_prompt_versions(version_file_path) + if revisions.current_revision == version: + print( + f"Prompt {prompt_file_name} {version} is currently being used. " + "Please switch to another version first." + ) + return + + revision_file_path = find_prompt_path(prompt_versions_directory, version) + if not revision_file_path: + raise FileNotFoundError( + f"Prompt version {version} not found in {prompt_versions_directory}" + ) + # TODO: Implement rollback in case of failure + os.remove(revision_file_path) + + # Detach any revisions that had the deleted version as their prev_revision_id + revision_file_paths = find_prompt_paths(prompt_versions_directory, "") + if revision_file_paths is None: + revision_file_paths = [] + for revision_file_path in revision_file_paths: + prev_revision_id_found = False + with open( + revision_file_path, + "r", + encoding="utf-8", + ) as file: + lines = file.readlines() + for i, line in enumerate(lines): + if "prev_revision_id" in line: + current_prev_revision_id = line.strip().split("=")[1].strip() + current_prev_revision_id = current_prev_revision_id[1:-1] + if current_prev_revision_id == version: + lines[i] = "prev_revision_id = None\n" + prev_revision_id_found = True + break + if prev_revision_id_found: + with open(revision_file_path, "w", encoding="utf-8") as file: + file.writelines(lines) + print(f"Detached {revision_file_path}") + print(f"Prompt {prompt_file_name} {version} successfully removed") diff --git a/mirascope_cli/commands/status.py b/mirascope_cli/commands/status.py new file mode 100644 index 0000000..9e57a56 --- /dev/null +++ b/mirascope_cli/commands/status.py @@ -0,0 +1,57 @@ +"""The status command for the Mirascope CLI.""" +import os +from typing import Optional + +from typer import Argument + +from ..utils import ( + check_status, + get_user_mirascope_settings, + parse_prompt_file_name, + prompts_directory_files, +) + + +def status_command( + prompt_file_name: Optional[str] = Argument( + help="Prompt to check status on", + autocompletion=prompts_directory_files, + parser=parse_prompt_file_name, + default=None, + ), +) -> None: + """Checks the status of the current prompt or prompts. + + If a prompt is specified, the status of that prompt is checked. Otherwise, the + status of all promps are checked. If a prompt has changed, the path to the prompt + is printed. + + Args: + prompt_file_name: (Optional) The name of the prompt file to check status on. + + Raises: + FileNotFoundError: If the file is not found in the specified prompts directory. + """ + mirascope_settings = get_user_mirascope_settings() + version_directory_path = mirascope_settings.versions_location + + # If a prompt is specified, check the status of that prompt + if prompt_file_name: + used_prompt_path = check_status(mirascope_settings, prompt_file_name) + if used_prompt_path: + print(f"Prompt {used_prompt_path} has changed.") + else: + print("No changes detected.") + else: # Otherwise, check the status of all prompts + directores_changed: list[str] = [] + for _, directories, _ in os.walk(version_directory_path): + for directory in directories: + used_prompt_path = check_status(mirascope_settings, directory) + if used_prompt_path: + directores_changed.append(used_prompt_path) + if len(directores_changed) > 0: + print("The following prompts have changed:") + for prompt in directores_changed: + print(f"\t{prompt}".expandtabs(4)) + else: + print("No changes detected.") diff --git a/mirascope_cli/commands/use.py b/mirascope_cli/commands/use.py new file mode 100644 index 0000000..afd4b1e --- /dev/null +++ b/mirascope_cli/commands/use.py @@ -0,0 +1,76 @@ +"""The use command for the Mirascope CLI.""" +import os + +from typer import Argument + +from ..constants import CURRENT_REVISION_KEY +from ..enums import MirascopeCommand +from ..utils import ( + check_status, + find_prompt_path, + get_user_mirascope_settings, + parse_prompt_file_name, + prompts_directory_files, + run_format_command, + update_version_text_file, + write_prompt_to_template, +) + + +def use_command( + prompt_file_name: str = Argument( + help="Prompt file to use", + autocompletion=prompts_directory_files, + parser=parse_prompt_file_name, + ), + version: str = Argument( + help="Version of prompt to use", + ), +) -> None: + """Uses the version and prompt specified by the user. + + The contents of the prompt in the versions directory are copied to the user's + prompts directory, based on the version specified by the user. The version file is + updated with the new revision. + + Args: + prompt_file_name: The name of the prompt file to use. + version: The version of the prompt file to use. + + Raises: + FileNotFoundError: If the file is not found in the versions directory. + """ + mirascope_settings = get_user_mirascope_settings() + used_prompt_path = check_status(mirascope_settings, prompt_file_name) + # Check status before continuing + if used_prompt_path: + print("Changes detected, please add or remove changes first.") + print(f"\tmirascope add {prompt_file_name}".expandtabs(4)) + return + version_directory_path = mirascope_settings.versions_location + prompt_directory_path = mirascope_settings.prompts_location + version_file_name = mirascope_settings.version_file_name + prompt_versions_directory = os.path.join(version_directory_path, prompt_file_name) + revision_file_path = find_prompt_path(prompt_versions_directory, version) + version_file_path = os.path.join(prompt_versions_directory, version_file_name) + if revision_file_path is None: + raise FileNotFoundError( + f"Prompt version {version} not found in {prompt_versions_directory}" + ) + # Open versioned prompt file + with open(revision_file_path, "r", encoding="utf-8") as file: + content = file.read() + # Write to user's prompt file + prompt_file_path = os.path.join(prompt_directory_path, f"{prompt_file_name}.py") + with open(prompt_file_path, "w+", encoding="utf-8") as file2: + file2.write(write_prompt_to_template(content, MirascopeCommand.USE)) + if prompt_file_path: + run_format_command(prompt_file_path) + + # Update version file with new current revision + keys_to_update = { + CURRENT_REVISION_KEY: version, + } + update_version_text_file(version_file_path, keys_to_update) + + print(f"Using {revision_file_path}") diff --git a/mirascope_cli/constants.py b/mirascope_cli/constants.py new file mode 100644 index 0000000..514696d --- /dev/null +++ b/mirascope_cli/constants.py @@ -0,0 +1,4 @@ +"""Constants for Mirascope CLI.""" + +CURRENT_REVISION_KEY = "CURRENT_REVISION" +LATEST_REVISION_KEY = "LATEST_REVISION" diff --git a/mirascope_cli/enums.py b/mirascope_cli/enums.py new file mode 100644 index 0000000..b6f1e57 --- /dev/null +++ b/mirascope_cli/enums.py @@ -0,0 +1,33 @@ +"""Enum Classes for Mirascope CLI.""" +from enum import Enum, EnumMeta +from typing import Any + + +class _Metaclass(EnumMeta): + """Base `EnumMeta` subclass for accessing enum members directly.""" + + def __getattribute__(cls, __name: str) -> Any: + value = super().__getattribute__(__name) + if isinstance(value, Enum): + value = value.value + return value + + +class _Enum(str, Enum, metaclass=_Metaclass): + """Base Enum Class.""" + + +class MirascopeCommand(_Enum): + """CLI commands to be executed. + + - ADD: save a modified prompt to the `versions/` folder. + - USE: load a specific version of the prompt from `versions/` as the current prompt. + - STATUS: display if any changes have been made to prompts, and if a prompt is + specified, displays changes for only said prompt. + - INIT: initializes the necessary folders for prompt versioning with CLI. + """ + + ADD = "add" + USE = "use" + STATUS = "status" + INIT = "init" diff --git a/mirascope_cli/generic/__init__.py b/mirascope_cli/generic/__init__.py new file mode 100644 index 0000000..7c6757d --- /dev/null +++ b/mirascope_cli/generic/__init__.py @@ -0,0 +1 @@ +"""This module contains generic templates used to initialize a mirascope project. """ diff --git a/mirascope_cli/generic/mirascope.ini.j2 b/mirascope_cli/generic/mirascope.ini.j2 new file mode 100644 index 0000000..3f1486a --- /dev/null +++ b/mirascope_cli/generic/mirascope.ini.j2 @@ -0,0 +1,20 @@ +[mirascope] + +#path to mirascope directory +mirascope_location = {{ mirascope_location }} + +# path to versions directory +versions_location = %(mirascope_location)s/{{ versions_location }} + +# path to prompts directory +prompts_location = {{ prompts_location }} + +# name of versions text file +version_file_name = {{ version_file_name }} + +# formats the version file +# leave blank to not format +format_command = ruff check --select I --fix; ruff format + +# auto tag prompts with version +auto_tag = True diff --git a/mirascope_cli/generic/prompt_template.j2 b/mirascope_cli/generic/prompt_template.j2 new file mode 100644 index 0000000..d518e27 --- /dev/null +++ b/mirascope_cli/generic/prompt_template.j2 @@ -0,0 +1,53 @@ +{%- if comments -%} +"""{{ comments }}""" +{%- endif -%} +{%- if imports -%} +{%- for import, alias in imports -%} +{%- if alias %} +import {{ import }} as {{alias}} +{%- else %} +import {{ import }} +{%- endif -%} +{%- endfor -%} +{%- endif %} + +{% if from_imports -%} +{%- set from_import_groups = {} -%} +{%- for module, name, alias in from_imports -%} + {% if module not in from_import_groups %} + {%- set _ = from_import_groups.update({module: [(name, alias)]}) -%} + {%- else -%} + {%- set _ = from_import_groups[module].append((name, alias)) -%} + {%- endif -%} +{%- endfor -%} +{% for module, names in from_import_groups.items() -%} +from {{ module }} import {% for name, alias in names %}{% if alias %}{{ name }} as {{ alias }}{% else %}{{ name }}{% endif %}{% if not loop.last %}, {% endif %}{% endfor %} +{% endfor %} +{% endif -%} + +{% if variables %} +{%- for var_name, var_value in variables.items() -%} +{{ var_name }} = {{ var_value }} +{% endfor %} +{% endif -%} +{% for class in classes -%} +{%- for decorator in class.decorators %} +@{{ decorator }} +{%- endfor %} +class {{ class.name }}({{ class.bases | join(', ') }}): +{%- if class.docstring %} + """{{ class.docstring }}""" +{% endif %} + +{%- for line in class.body.split('\n') %} +{%- if line.startswith('def ') or line.startswith('@') %} + {%- if previous_line_was_function %} + + {%- endif %} + {%- set previous_line_was_function = true %} + {%- else %} + {%- set previous_line_was_function = false %} + {%- endif %} + {{ line }} +{%- endfor %} +{% endfor %} diff --git a/mirascope_cli/main.py b/mirascope_cli/main.py new file mode 100644 index 0000000..a73630c --- /dev/null +++ b/mirascope_cli/main.py @@ -0,0 +1,43 @@ +"""The Mirascope CLI prompt management tool. + +Typical usage example: + + Initialize the environment: + $ mirascope init mirascope + + Create a prompt in the prompts directory: + prompts/my_prompt.py + + Add the prompt to create a version: + $ mirascope add my_prompt + + Iterate on the prompt in the prompts directory: + + Check the status of the prompt: + $ mirascope status my_prompt + + Add the prompt to create a new version: + $ mirascope add my_prompt + + Switch between prompts: + $ mirascope use my_prompt 0001 +""" + + +from typer import Typer + +from .commands import ( + add_command, + init_command, + remove_command, + status_command, + use_command, +) + +app = Typer() + +app.command(name="add", help="Add a prompt")(add_command) +app.command(name="status", help="Check status of prompt(s)")(status_command) +app.command(name="use", help="Use a prompt")(use_command) +app.command(name="remove", help="Remove a prompt")(remove_command) +app.command(name="init", help="Initialize mirascope project")(init_command) diff --git a/mirascope_cli/py.typed b/mirascope_cli/py.typed new file mode 100644 index 0000000..e69de29 diff --git a/mirascope_cli/schemas.py b/mirascope_cli/schemas.py new file mode 100644 index 0000000..d899356 --- /dev/null +++ b/mirascope_cli/schemas.py @@ -0,0 +1,49 @@ +"""Contains the schema for files created by the mirascope cli.""" +from typing import Optional + +from pydantic import BaseModel, ConfigDict, Field + + +class MirascopeSettings(BaseModel): + """Model for the user's mirascope settings.""" + + mirascope_location: str + versions_location: str + prompts_location: str + version_file_name: str + format_command: Optional[str] = None + auto_tag: Optional[bool] = None + + model_config = ConfigDict(extra="forbid") + + +class VersionTextFile(BaseModel): + """Model for the version text file.""" + + current_revision: Optional[str] = Field(default=None) + latest_revision: Optional[str] = Field(default=None) + + +class MirascopeCliVariables(BaseModel): + """Prompt version variables used internally by mirascope.""" + + prev_revision_id: Optional[str] = Field(default=None) + revision_id: Optional[str] = Field(default=None) + + +class ClassInfo(BaseModel): + name: str + bases: list[str] + body: str + decorators: list[str] + docstring: Optional[str] + + +class FunctionInfo(BaseModel): + name: str + args: list[str] + returns: Optional[str] + body: str + decorators: list[str] + docstring: Optional[str] + is_async: bool diff --git a/mirascope_cli/utils.py b/mirascope_cli/utils.py new file mode 100644 index 0000000..8066307 --- /dev/null +++ b/mirascope_cli/utils.py @@ -0,0 +1,684 @@ +"""Utility functions for the mirascope library.""" + +from __future__ import annotations + +import ast +import glob +import json +import os +import subprocess +import sys +from configparser import ConfigParser, MissingSectionHeaderError +from pathlib import Path +from typing import Any, Literal, Optional, Union + +from jinja2 import Environment, FileSystemLoader + +from .enums import MirascopeCommand +from .constants import CURRENT_REVISION_KEY, LATEST_REVISION_KEY +from .schemas import ( + ClassInfo, + FunctionInfo, + MirascopeCliVariables, + MirascopeSettings, + VersionTextFile, +) + +ignore_variables = {"prev_revision_id", "revision_id"} +mirascope_prompt_bases = ( + "BasePrompt", + "AnthropicCall", + "AnthropicExtractor", + "CohereCall", + "CohereExtractor", + "GeminiCall", + "GeminiExtractor", + "GroqCall", + "GroqExtractor", + "MistralCall", + "MistralExtractor", + "OpenAICall", + "OpenAIExtractor", +) + + +class PromptAnalyzer(ast.NodeVisitor): + """Utility class for analyzing a Mirascope prompt file. + + The call to `ast.parse()` returns Python code as an AST, whereby each visitor method + will be called for the corresponding nodes in the AST via `NodeVisitor.visit()`. + + Example: + + ```python + analyzer = PromptAnalyzer() + tree = ast.parse(file.read()) + analyzer.visit(tree) + ``` + + """ + + def __init__(self) -> None: + """Initializes the PromptAnalyzer.""" + self.imports: list[tuple[str, Optional[str]]] = [] + self.from_imports: list[tuple[str, str, Optional[str]]] = [] + self.variables: dict[str, Any] = {} + self.classes: list[ClassInfo] = [] + self.functions: list[FunctionInfo] = [] + self.comments: str = "" + + def visit_Import(self, node) -> None: + """Extracts imports from the given node.""" + for alias in node.names: + self.imports.append((alias.name, alias.asname)) + self.generic_visit(node) + + def visit_ImportFrom(self, node) -> None: + """Extracts from imports from the given node.""" + for alias in node.names: + self.from_imports.append((node.module, alias.name, alias.asname)) + self.generic_visit(node) + + def visit_Assign(self, node) -> None: + """Extracts variables from the given node.""" + target = node.targets[0] + if isinstance(target, ast.Name): + self.variables[target.id] = ast.unparse(node.value) + self.generic_visit(node) + + def visit_ClassDef(self, node) -> None: + """Extracts classes from the given node.""" + class_info = ClassInfo( + name=node.name, + bases=[ast.unparse(base) for base in node.bases], + body="", + decorators=[ast.unparse(decorator) for decorator in node.decorator_list], + docstring=None, + ) + + # Extract docstring if present + docstring = ast.get_docstring(node, False) + if docstring: + class_info.docstring = docstring + + # Handle the rest of the class body + body_nodes = [n for n in node.body if not isinstance(n, ast.Expr)] + body = [] + for node in body_nodes: + if ( + isinstance(node, ast.Assign) + and isinstance(node.targets[0], ast.Name) + and node.targets[0].id == "prompt_template" + and isinstance(node.value, ast.Constant) + and node.end_lineno is not None + and node.lineno < node.end_lineno + ): + # reconstruct template strings to be multi-line + lines = node.value.s.split("\n") + body.append(f'{node.targets[0].id} = """{lines.pop(0).strip()}') + for i, line in enumerate(lines): + stripped_line = line.strip() + if stripped_line or i < len(lines) - 1: + body.append(line.strip()) + body.append('"""') + body.append("") # adds final newline + else: + body.append(ast.unparse(node)) + class_info.body = "\n".join(body) + + self.classes.append(class_info) + + def visit_AsyncFunctionDef(self, node): + """Extracts async functions from the given node.""" + return self._visit_Function(node, is_async=True) + + def visit_FunctionDef(self, node): + """Extracts functions from the given node.""" + return self._visit_Function(node, is_async=False) + + def _visit_Function(self, node, is_async): + """Extracts functions or async functions from the given node.""" + # Initial function information setup + function_info = FunctionInfo( + name=node.name, + args=[ast.unparse(arg) for arg in node.args.args], + returns=ast.unparse(node.returns) if node.returns else None, + body="", + decorators=[ast.unparse(decorator) for decorator in node.decorator_list], + docstring=None, + is_async=is_async, # Indicates whether the function is async + ) + + # Extract docstring if present + docstring = ast.get_docstring(node, False) + if docstring: + function_info.docstring = docstring + + # Handle the rest of the function body + body_nodes = [n for n in node.body if not isinstance(n, ast.Expr)] + function_info.body = "\n".join(ast.unparse(n) for n in body_nodes) + + # Assuming you have a list to store functions + self.functions.append(function_info) + + def visit_Module(self, node) -> None: + """Extracts comments from the given node.""" + comments = ast.get_docstring(node, False) + self.comments = "" if comments is None else comments + self.generic_visit(node) + + def check_function_changed(self, other: PromptAnalyzer) -> bool: + """Compares the functions of this file with those of another file.""" + return self._check_definition_changed(other, "function") + + def check_class_changed(self, other: PromptAnalyzer) -> bool: + """Compares the classes of this file with those of another file.""" + return self._check_definition_changed(other) + + def _check_definition_changed( + self, + other: PromptAnalyzer, + definition_type: Optional[Literal["class", "function"]] = "class", + ) -> bool: + """Compares classes or the functions of this file with those of another file""" + + self_definitions: Union[list[ClassInfo], list[FunctionInfo]] = ( + self.classes if definition_type == "class" else self.functions + ) + other_definitions: Union[list[ClassInfo], list[FunctionInfo]] = ( + other.classes if definition_type == "class" else other.functions + ) + + self_definitions_dict = { + definition.name: definition for definition in self_definitions + } + other_definitions_dict = { + definition.name: definition for definition in other_definitions + } + + all_definition_names = set(self_definitions_dict.keys()) | set( + other_definitions_dict.keys() + ) + + for name in all_definition_names: + if name in self_definitions_dict and name in other_definitions_dict: + self_def_dict = self_definitions_dict[name].__dict__ + other_def_dict = other_definitions_dict[name].__dict__ + # Compare attributes of definitions with the same name + def_diff = { + attr: (self_def_dict[attr], other_def_dict[attr]) + for attr in self_def_dict + if self_def_dict[attr] != other_def_dict[attr] + } + if def_diff: + return True + else: + return True + + return False + + +def get_user_mirascope_settings( + ini_file_path: str = "mirascope.ini", +) -> MirascopeSettings: + """Returns the user's mirascope settings. + + Args: + ini_file_path: The path to the mirascope.ini file. + + Returns: + The user's mirascope settings as a `MirascopeSettings` instance. + + Raises: + FileNotFoundError: If the mirascope.ini file is not found. + KeyError: If the [mirascope] section is missing from the mirascope.ini file. + """ + config = ConfigParser(allow_no_value=True) + try: + read_ok = config.read(ini_file_path) + if not read_ok: + raise FileNotFoundError( + "The mirascope.ini file was not found. Please run " + "`mirascope init` to create one or run the mirascope CLI from the " + "same directory as the mirascope.ini file." + ) + mirascope_config = config["mirascope"] + return MirascopeSettings(**mirascope_config) + except MissingSectionHeaderError as e: + raise MissingSectionHeaderError(ini_file_path, e.lineno, e.source) from e + + +def prompts_directory_files() -> list[str]: + """Returns a list of files in the user's prompts directory.""" + mirascope_settings = get_user_mirascope_settings() + prompt_file_names = find_file_names(mirascope_settings.prompts_location) + return [f"{name[:-3]}" for name in prompt_file_names] # remove .py extension + + +def parse_prompt_file_name(prompt_file_name: str) -> str: + """Returns the file name without the .py extension.""" + if prompt_file_name.endswith(".py"): + return prompt_file_name[:-3] + return prompt_file_name + + +def get_prompt_versions(version_file_path: str) -> VersionTextFile: + """Returns the versions of the given prompt. + + Args: + version_file_path: The path to the prompt. + + Returns: + A `VersionTextFile` instance with the versions of current and latest revisions. + """ + versions = VersionTextFile() + try: + with open(version_file_path, "r", encoding="utf-8") as file: + file.seek(0) + for line in file: + # Check if the current line contains the key + if line.startswith(CURRENT_REVISION_KEY + "="): + versions.current_revision = line.split("=")[1].strip() + elif line.startswith(LATEST_REVISION_KEY + "="): + versions.latest_revision = line.split("=")[1].strip() + return versions + except FileNotFoundError: + return versions + + +def check_prompt_changed(file1_path: Optional[str], file2_path: Optional[str]) -> bool: + """Compare two prompts to check if the given prompts have changed. + + Args: + file1_path: The path to the first prompt. + file2_path: The path to the second prompt. + + Returns: + Whether there are any differences between the two prompts. + """ + if file1_path is None or file2_path is None: + raise FileNotFoundError("Prompt or version file is missing.") + # Parse the first file + try: + with open(file1_path, "r", encoding="utf-8") as file: + content = file.read() + except FileNotFoundError as e: + raise FileNotFoundError(f"The file {file1_path} was not found.") from e + analyzer1 = PromptAnalyzer() + tree1 = ast.parse(content) + analyzer1.visit(tree1) + + # Parse the second file + try: + with open(file2_path, "r", encoding="utf-8") as file: + content = file.read() + except FileNotFoundError as e: + raise FileNotFoundError(f"The file {file2_path} was not found.") from e + analyzer2 = PromptAnalyzer() + tree2 = ast.parse(content) + analyzer2.visit(tree2) + # Compare the contents of the two files + differences = { + "comments": analyzer1.comments != analyzer2.comments, + "imports_diff": bool(set(analyzer1.imports) ^ set(analyzer2.imports)), + "from_imports_diff": bool( + set(analyzer1.from_imports) ^ set(analyzer2.from_imports) + ), + "functions_diff": analyzer1.check_function_changed(analyzer2), + "variables_diff": set(analyzer1.variables.keys()) - ignore_variables + ^ set(analyzer2.variables.keys()) - ignore_variables, + "classes_diff": analyzer1.check_class_changed(analyzer2), + # Add other comparisons as needed + } + return any(differences.values()) + + +def find_file_names(directory: str, prefix: str = "") -> list[str]: + """Finds all files in a directory. + + Args: + directory: The directory to search for the prompt. + prefix: The prefix of the prompt to search for. + + Returns: + A list of file names found. + """ + pattern = os.path.join(directory, f"[!_]{prefix}*.py") # ignores private files + matching_files_with_dir = glob.glob(pattern) + + # Removing the directory part from each path + return [os.path.basename(file) for file in matching_files_with_dir] + + +def find_prompt_paths(directory: Union[Path, str], prefix: str) -> Optional[list[str]]: + """Finds and opens all prompts with the given directory. + + Args: + directory: The directory to search for the prompt. + prefix: The prefix of the prompt to search for. + + Returns: + A list of paths to the prompt. + """ + pattern = os.path.join(directory, prefix + "*.py") + prompt_files = glob.glob(pattern) + + if not prompt_files: + return None # No files found + + # Return first file found + return prompt_files + + +def find_prompt_path(directory: Union[Path, str], prefix: str) -> Optional[str]: + """Finds and opens the first found prompt with the given directory. + + Args: + directory: The directory to search for the prompt. + prefix: The prefix of the prompt to search for. + + Returns: + The path to the prompt. + """ + prompt_files = find_prompt_paths(directory, prefix) + if prompt_files: + return prompt_files[0] + return None + + +def get_prompt_analyzer(file: str) -> PromptAnalyzer: + """Gets an instance of PromptAnalyzer for a file + + Args: + file: The file to analyze + + Returns: + An instance of PromptAnalyzer + """ + analyzer = PromptAnalyzer() + tree = ast.parse(file) + analyzer.visit(tree) + return analyzer + + +def _find_list_from_str(string: str) -> Optional[list[str]]: + """Finds a list from a string. + + Args: + string: The string to find the list from. + + Returns: + The list found from the string. + """ + start_bracket_index = string.find("[") + end_bracket_index = string.find("]") + if ( + start_bracket_index != -1 + and end_bracket_index != -1 + and end_bracket_index > start_bracket_index + ): + new_list = string[start_bracket_index : end_bracket_index + 1] + return json.loads(new_list.replace("'", '"')) + return None + + +def _update_tag_decorator_with_version( + decorators: list[str], variables: MirascopeCliVariables, mirascope_alias: str +) -> Optional[str]: + """Updates the tag decorator and returns the import name. + + Args: + decorators: The decorators of the prompt. + variables: The variables used by mirascope internal. + mirascope_alias: The alias of the mirascope module. + + Returns: + The import name of the tag decorator. + """ + if variables.revision_id is None: + return None + import_name = "tags" + tag_exists = False + version_tag_prefix = "version:" # mirascope tag prefix + version_tag = f"{version_tag_prefix}{variables.revision_id}" + for index, decorator in enumerate(decorators): + if any( + decorator.startswith(prefix) + for prefix in ("tags(", f"{mirascope_alias}.tags(") + ): + tag_exists = True + import_name = decorator.split("(")[0] + decorator_arguments = _find_list_from_str(decorator) + if decorator_arguments is not None: + if f"{version_tag}" in decorator_arguments: + # The version tag already exists + break + elif any( + argument.startswith(version_tag_prefix) + for argument in decorator_arguments + ): + # Replace the version tag with the current version + for i, word in enumerate(decorator_arguments): + if word.startswith(version_tag_prefix): + decorator_arguments[i] = f"{version_tag}" + decorators[index] = f"{import_name}({decorator_arguments})" + else: + # Tag decorator exists, append the current version to the tags + decorator_arguments.append(f"{version_tag}") + decorators[index] = f"{import_name}({decorator_arguments})" + else: + # Add tags decorator + decorators[index] = f"{import_name}({version_tag})" + break + if not tag_exists: + decorators.append(f'{import_name}(["{version_tag}"])') + return import_name + + +def _update_mirascope_imports(imports: list[tuple[str, Optional[str]]]): + """Updates the mirascope import. + + Args: + imports: The imports from the PromptAnalyzer class + """ + if not any(import_name == "mirascope" for import_name, _ in imports): + imports.append(("mirascope", None)) + + +def _update_mirascope_from_imports( + member: str, from_imports: list[tuple[str, str, Optional[str]]] +): + """Updates the mirascope from imports. + + Args: + member: The member to import. + from_imports: The from imports from the PromptAnalyzer class + """ + if not any( + ( + module_name == "mirascope" + or module_name == "mirascope.base" + or module_name == "mirascope.prompts" + ) + and import_name == member + for module_name, import_name, _ in from_imports + ): + from_imports.append(("mirascope", member, None)) + + +def write_prompt_to_template( + file: str, + command: Literal[MirascopeCommand.ADD, MirascopeCommand.USE], + variables: Optional[MirascopeCliVariables] = None, +) -> str: + """Writes the given prompt to the template. + + Deconstructs a prompt with ast and reconstructs it using the Jinja2 template, adding + revision history into the prompt when the command is `MirascopeCommand.ADD`. + + Args: + file: The path to the prompt. + command: The CLI command to execute. + variables: A dictionary of revision ids which are rendered together with + variable assignments that are not inside any class. Only relevant when + `command` is `MirascopeCommand.ADD` - if `command` is + `MirascopeCommand.USE`, `variables` should be `None`. + + Returns: + The reconstructed prompt. + """ + mirascope_settings = get_user_mirascope_settings() + mirascope_directory = mirascope_settings.mirascope_location + auto_tag = mirascope_settings.auto_tag + template_loader = FileSystemLoader(searchpath=mirascope_directory) + template_env = Environment(loader=template_loader) + template = template_env.get_template("prompt_template.j2") + analyzer = get_prompt_analyzer(file) + if variables is None: + variables = MirascopeCliVariables() + + if command == MirascopeCommand.ADD: + # double quote revision ids to match how `ast.unparse()` formats strings + new_variables = { + k: f"'{v}'" if isinstance(v, str) else None + for k, v in variables.__dict__.items() + } | analyzer.variables + else: # command == MirascopeCommand.USE + ignore_variable_keys = dict.fromkeys(ignore_variables, None) + new_variables = { + k: analyzer.variables[k] + for k in analyzer.variables + if k not in ignore_variable_keys + } + + if auto_tag: + import_tag_name: Optional[str] = None + mirascope_alias = "mirascope" + for name, alias in analyzer.imports: + if name == "mirascope" and alias is not None: + mirascope_alias = alias + break + for module, name, alias in analyzer.from_imports: + if module == "mirascope" and name == "tags" and alias is not None: + mirascope_alias = alias + break + + for python_class in analyzer.classes: + decorators = python_class.decorators + if python_class.bases and python_class.bases[0] in mirascope_prompt_bases: + import_tag_name = _update_tag_decorator_with_version( + decorators, variables, mirascope_alias + ) + + if import_tag_name == "tags": + _update_mirascope_from_imports(import_tag_name, analyzer.from_imports) + elif import_tag_name == f"{mirascope_alias}.tags": + _update_mirascope_imports(analyzer.imports) + + data = { + "comments": analyzer.comments, + "variables": new_variables, + "imports": analyzer.imports, + "from_imports": analyzer.from_imports, + "classes": analyzer.classes, + } + return template.render(**data) + + +def update_version_text_file( + version_file: str, + updates: dict[str, str], +) -> None: + """Updates the version text file. + + Depending on the contents of `updates`, updates the ids of the current and latest + revisions of the prompt. + + Args: + version_file: The path to the version text file. + updates: A dictionary containing updates to the current revision id and/or + the latest revision id. + """ + modified_lines = [] + edits_made = { + key: False for key in updates + } # Track which keys already exist in the file + version_file_path: Path = Path(version_file) + if not version_file_path.is_file(): + version_file_path.touch() + # Read the file and apply updates + with open(version_file_path, "r", encoding="utf-8") as file: + for line in file: + # Check if the current line contains any of the keys + for key, value in updates.items(): + if line.startswith(key + "="): + modified_lines.append(f"{key}={value}\n") + edits_made[key] = True + break + else: + # No key found, so keep the line as is + modified_lines.append(line) + + # Add any keys that were not found at the end of the file + for key, value in updates.items(): + if not edits_made[key]: + modified_lines.append(f"{key}={value}\n") + + # Write the modified content back to the file + with open(version_file_path, "w", encoding="utf-8") as file: + file.writelines(modified_lines) + + +def check_status( + mirascope_settings: MirascopeSettings, directory: str +) -> Optional[str]: + """Checks the status of the given directory. + + Args: + mirascope_settings: The user's mirascope settings. + directory: The name of the prompt file (excluding the .py extension). + + Returns: + The path to the prompt if the prompt has changed, otherwise `None`. + """ + version_directory_path = mirascope_settings.versions_location + prompt_directory_path = mirascope_settings.prompts_location + version_file_name = mirascope_settings.version_file_name + prompt_directory = os.path.join(version_directory_path, directory) + used_prompt_path = f"{prompt_directory_path}/{directory}.py" + + # Get the currently used prompt version + versions = get_prompt_versions(f"{prompt_directory}/{version_file_name}") + current_head = versions.current_revision + if current_head is None: + return used_prompt_path + current_version_prompt_path = find_prompt_path(prompt_directory, current_head) + # Check if users prompt matches the current prompt version + has_file_changed = check_prompt_changed( + current_version_prompt_path, used_prompt_path + ) + if has_file_changed: + return used_prompt_path + return None + + +def run_format_command(file: str) -> None: + """Runs the format command on the given file. + + Args: + file: The file to format + """ + mirascope_settings = get_user_mirascope_settings() + if mirascope_settings.format_command: + format_commands: list[list[str]] = [ + command.split() for command in mirascope_settings.format_command.split(";") + ] + # assuming the final command takes filename as argument, and as final argument + format_commands[-1].append(file) + for command in format_commands: + subprocess.run( + [sys.executable, "-m"] + command, + check=True, + capture_output=True, + shell=False, + ) diff --git a/poetry.lock b/poetry.lock new file mode 100644 index 0000000..bc2dea8 --- /dev/null +++ b/poetry.lock @@ -0,0 +1,1377 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "babel" +version = "2.14.0" +description = "Internationalization utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "Babel-2.14.0-py3-none-any.whl", hash = "sha256:efb1a25b7118e67ce3a259bed20545c29cb68be8ad2c784c83689981b7a57287"}, + {file = "Babel-2.14.0.tar.gz", hash = "sha256:6919867db036398ba21eb5c7a0f6b28ab8cbc3ae7a73a44ebe34ae74a4e7d363"}, +] + +[package.extras] +dev = ["freezegun (>=1.0,<2.0)", "pytest (>=6.0)", "pytest-cov"] + +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.4.4" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.4.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e0be5efd5127542ef31f165de269f77560d6cdef525fffa446de6f7e9186cfb2"}, + {file = "coverage-7.4.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ccd341521be3d1b3daeb41960ae94a5e87abe2f46f17224ba5d6f2b8398016cf"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fa497a8ab37784fbb20ab699c246053ac294d13fc7eb40ec007a5043ec91f8"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b1a93009cb80730c9bca5d6d4665494b725b6e8e157c1cb7f2db5b4b122ea562"}, + {file = "coverage-7.4.4-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:690db6517f09336559dc0b5f55342df62370a48f5469fabf502db2c6d1cffcd2"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:09c3255458533cb76ef55da8cc49ffab9e33f083739c8bd4f58e79fecfe288f7"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:8ce1415194b4a6bd0cdcc3a1dfbf58b63f910dcb7330fe15bdff542c56949f87"}, + {file = "coverage-7.4.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:b91cbc4b195444e7e258ba27ac33769c41b94967919f10037e6355e998af255c"}, + {file = "coverage-7.4.4-cp310-cp310-win32.whl", hash = "sha256:598825b51b81c808cb6f078dcb972f96af96b078faa47af7dfcdf282835baa8d"}, + {file = "coverage-7.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:09ef9199ed6653989ebbcaacc9b62b514bb63ea2f90256e71fea3ed74bd8ff6f"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0f9f50e7ef2a71e2fae92774c99170eb8304e3fdf9c8c3c7ae9bab3e7229c5cf"}, + {file = "coverage-7.4.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:623512f8ba53c422fcfb2ce68362c97945095b864cda94a92edbaf5994201083"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0513b9508b93da4e1716744ef6ebc507aff016ba115ffe8ecff744d1322a7b63"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40209e141059b9370a2657c9b15607815359ab3ef9918f0196b6fccce8d3230f"}, + {file = "coverage-7.4.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8a2b2b78c78293782fd3767d53e6474582f62443d0504b1554370bde86cc8227"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:73bfb9c09951125d06ee473bed216e2c3742f530fc5acc1383883125de76d9cd"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1f384c3cc76aeedce208643697fb3e8437604b512255de6d18dae3f27655a384"}, + {file = "coverage-7.4.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:54eb8d1bf7cacfbf2a3186019bcf01d11c666bd495ed18717162f7eb1e9dd00b"}, + {file = "coverage-7.4.4-cp311-cp311-win32.whl", hash = "sha256:cac99918c7bba15302a2d81f0312c08054a3359eaa1929c7e4b26ebe41e9b286"}, + {file = "coverage-7.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:b14706df8b2de49869ae03a5ccbc211f4041750cd4a66f698df89d44f4bd30ec"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:201bef2eea65e0e9c56343115ba3814e896afe6d36ffd37bab783261db430f76"}, + {file = "coverage-7.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41c9c5f3de16b903b610d09650e5e27adbfa7f500302718c9ffd1c12cf9d6818"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d898fe162d26929b5960e4e138651f7427048e72c853607f2b200909794ed978"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ea79bb50e805cd6ac058dfa3b5c8f6c040cb87fe83de10845857f5535d1db70"}, + {file = "coverage-7.4.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce4b94265ca988c3f8e479e741693d143026632672e3ff924f25fab50518dd51"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:00838a35b882694afda09f85e469c96367daa3f3f2b097d846a7216993d37f4c"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:fdfafb32984684eb03c2d83e1e51f64f0906b11e64482df3c5db936ce3839d48"}, + {file = "coverage-7.4.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:69eb372f7e2ece89f14751fbcbe470295d73ed41ecd37ca36ed2eb47512a6ab9"}, + {file = "coverage-7.4.4-cp312-cp312-win32.whl", hash = "sha256:137eb07173141545e07403cca94ab625cc1cc6bc4c1e97b6e3846270e7e1fea0"}, + {file = "coverage-7.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:d71eec7d83298f1af3326ce0ff1d0ea83c7cb98f72b577097f9083b20bdaf05e"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d5ae728ff3b5401cc320d792866987e7e7e880e6ebd24433b70a33b643bb0384"}, + {file = "coverage-7.4.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:cc4f1358cb0c78edef3ed237ef2c86056206bb8d9140e73b6b89fbcfcbdd40e1"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8130a2aa2acb8788e0b56938786c33c7c98562697bf9f4c7d6e8e5e3a0501e4a"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf271892d13e43bc2b51e6908ec9a6a5094a4df1d8af0bfc360088ee6c684409"}, + {file = "coverage-7.4.4-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4cdc86d54b5da0df6d3d3a2f0b710949286094c3a6700c21e9015932b81447e"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:ae71e7ddb7a413dd60052e90528f2f65270aad4b509563af6d03d53e979feafd"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:38dd60d7bf242c4ed5b38e094baf6401faa114fc09e9e6632374388a404f98e7"}, + {file = "coverage-7.4.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:aa5b1c1bfc28384f1f53b69a023d789f72b2e0ab1b3787aae16992a7ca21056c"}, + {file = "coverage-7.4.4-cp38-cp38-win32.whl", hash = "sha256:dfa8fe35a0bb90382837b238fff375de15f0dcdb9ae68ff85f7a63649c98527e"}, + {file = "coverage-7.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:b2991665420a803495e0b90a79233c1433d6ed77ef282e8e152a324bbbc5e0c8"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3b799445b9f7ee8bf299cfaed6f5b226c0037b74886a4e11515e569b36fe310d"}, + {file = "coverage-7.4.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b4d33f418f46362995f1e9d4f3a35a1b6322cb959c31d88ae56b0298e1c22357"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadacf9a2f407a4688d700e4ebab33a7e2e408f2ca04dbf4aef17585389eff3e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7c95949560050d04d46b919301826525597f07b33beba6187d04fa64d47ac82e"}, + {file = "coverage-7.4.4-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ff7687ca3d7028d8a5f0ebae95a6e4827c5616b31a4ee1192bdfde697db110d4"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:5fc1de20b2d4a061b3df27ab9b7c7111e9a710f10dc2b84d33a4ab25065994ec"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:c74880fc64d4958159fbd537a091d2a585448a8f8508bf248d72112723974cbd"}, + {file = "coverage-7.4.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:742a76a12aa45b44d236815d282b03cfb1de3b4323f3e4ec933acfae08e54ade"}, + {file = "coverage-7.4.4-cp39-cp39-win32.whl", hash = "sha256:d89d7b2974cae412400e88f35d86af72208e1ede1a541954af5d944a8ba46c57"}, + {file = "coverage-7.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:9ca28a302acb19b6af89e90f33ee3e1906961f94b54ea37de6737b7ca9d8827c"}, + {file = "coverage-7.4.4-pp38.pp39.pp310-none-any.whl", hash = "sha256:b2c5edc4ac10a7ef6605a966c58929ec6c1bd0917fb8c15cb3363f65aa40e677"}, + {file = "coverage-7.4.4.tar.gz", hash = "sha256:c901df83d097649e257e803be22592aedfd5182f07b3cc87d640bbb9afd50f49"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + +[[package]] +name = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + +[[package]] +name = "griffe" +version = "0.42.2" +description = "Signatures for entire Python programs. Extract the structure, the frame, the skeleton of your project, to generate API documentation or find breaking changes in your API." +optional = false +python-versions = ">=3.8" +files = [ + {file = "griffe-0.42.2-py3-none-any.whl", hash = "sha256:bf9a09d7e9dcc3aca6a2c7ab4f63368c19e882f58c816fbd159bea613daddde3"}, + {file = "griffe-0.42.2.tar.gz", hash = "sha256:d5547b7a1a0786f84042379a5da8bd97c11d0464d4de3d7510328ebce5fda772"}, +] + +[package.dependencies] +colorama = ">=0.4" + +[[package]] +name = "idna" +version = "3.7" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.7-py3-none-any.whl", hash = "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0"}, + {file = "idna-3.7.tar.gz", hash = "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc"}, +] + +[[package]] +name = "importlib-metadata" +version = "7.1.0" +description = "Read metadata from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_metadata-7.1.0-py3-none-any.whl", hash = "sha256:30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570"}, + {file = "importlib_metadata-7.1.0.tar.gz", hash = "sha256:b78938b926ee8d5f020fc4772d487045805a55ddbad2ecf21c6d60938dc7fcd2"}, +] + +[package.dependencies] +zipp = ">=0.5" + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +perf = ["ipython"] +testing = ["flufl.flake8", "importlib-resources (>=1.3)", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-perf (>=0.9.2)", "pytest-ruff (>=0.2.1)"] + +[[package]] +name = "importlib-resources" +version = "6.4.0" +description = "Read resources from Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "importlib_resources-6.4.0-py3-none-any.whl", hash = "sha256:50d10f043df931902d4194ea07ec57960f66a80449ff867bfe782b4c486ba78c"}, + {file = "importlib_resources-6.4.0.tar.gz", hash = "sha256:cdb2b453b8046ca4e3798eb1d84f3cce1446a0e8e7b5ef4efb600f19fc398145"}, +] + +[package.dependencies] +zipp = {version = ">=3.1.0", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (<7.2.5)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["jaraco.test (>=5.4)", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-mypy", "pytest-ruff (>=0.2.1)", "zipp (>=3.17)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + +[[package]] +name = "jinja2" +version = "3.1.3" +description = "A very fast and expressive template engine." +optional = false +python-versions = ">=3.7" +files = [ + {file = "Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa"}, + {file = "Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90"}, +] + +[package.dependencies] +MarkupSafe = ">=2.0" + +[package.extras] +i18n = ["Babel (>=2.7)"] + +[[package]] +name = "markdown" +version = "3.5.2" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "Markdown-3.5.2-py3-none-any.whl", hash = "sha256:d43323865d89fc0cb9b20c75fc8ad313af307cc087e84b657d9eec768eddeadd"}, + {file = "Markdown-3.5.2.tar.gz", hash = "sha256:e1ac7b3dc550ee80e602e71c1d168002f062e49f1b11e26a36264dafd4df2ef8"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.4", markers = "python_version < \"3.10\""} + +[package.extras] +docs = ["mdx-gh-links (>=0.2)", "mkdocs (>=1.5)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +description = "Python port of markdown-it. Markdown parsing, done right!" +optional = false +python-versions = ">=3.8" +files = [ + {file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"}, + {file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"}, +] + +[package.dependencies] +mdurl = ">=0.1,<1.0" + +[package.extras] +benchmarking = ["psutil", "pytest", "pytest-benchmark"] +code-style = ["pre-commit (>=3.0,<4.0)"] +compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "mistletoe (>=1.0,<2.0)", "mistune (>=2.0,<3.0)", "panflute (>=2.3,<3.0)"] +linkify = ["linkify-it-py (>=1,<3)"] +plugins = ["mdit-py-plugins"] +profiling = ["gprof2dot"] +rtd = ["jupyter_sphinx", "mdit-py-plugins", "myst-parser", "pyyaml", "sphinx", "sphinx-copybutton", "sphinx-design", "sphinx_book_theme"] +testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions"] + +[[package]] +name = "markupsafe" +version = "2.1.5" +description = "Safely add untrusted strings to HTML/XML markup." +optional = false +python-versions = ">=3.7" +files = [ + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win32.whl", hash = "sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4"}, + {file = "MarkupSafe-2.1.5-cp310-cp310-win_amd64.whl", hash = "sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win32.whl", hash = "sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906"}, + {file = "MarkupSafe-2.1.5-cp311-cp311-win_amd64.whl", hash = "sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win32.whl", hash = "sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad"}, + {file = "MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win32.whl", hash = "sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371"}, + {file = "MarkupSafe-2.1.5-cp37-cp37m-win_amd64.whl", hash = "sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win32.whl", hash = "sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff"}, + {file = "MarkupSafe-2.1.5-cp38-cp38-win_amd64.whl", hash = "sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win32.whl", hash = "sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf"}, + {file = "MarkupSafe-2.1.5-cp39-cp39-win_amd64.whl", hash = "sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5"}, + {file = "MarkupSafe-2.1.5.tar.gz", hash = "sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b"}, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +description = "Markdown URL utilities" +optional = false +python-versions = ">=3.7" +files = [ + {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, + {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mike" +version = "2.0.0" +description = "Manage multiple versions of your MkDocs-powered documentation" +optional = false +python-versions = "*" +files = [ + {file = "mike-2.0.0-py3-none-any.whl", hash = "sha256:87f496a65900f93ba92d72940242b65c86f3f2f82871bc60ebdcffc91fad1d9e"}, + {file = "mike-2.0.0.tar.gz", hash = "sha256:566f1cab1a58cc50b106fb79ea2f1f56e7bfc8b25a051e95e6eaee9fba0922de"}, +] + +[package.dependencies] +importlib-metadata = "*" +importlib-resources = "*" +jinja2 = ">=2.7" +mkdocs = ">=1.0" +pyparsing = ">=3.0" +pyyaml = ">=5.1" +verspec = "*" + +[package.extras] +dev = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] +test = ["coverage", "flake8 (>=3.0)", "flake8-quotes", "shtab"] + +[[package]] +name = "mkdocs" +version = "1.5.3" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocs-1.5.3-py3-none-any.whl", hash = "sha256:3b3a78e736b31158d64dbb2f8ba29bd46a379d0c6e324c2246c3bc3d2189cfc1"}, + {file = "mkdocs-1.5.3.tar.gz", hash = "sha256:eb7c99214dcb945313ba30426c2451b735992c73c2e10838f76d09e39ff4d0e2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +importlib-metadata = {version = ">=4.3", markers = "python_version < \"3.10\""} +jinja2 = ">=2.11.1" +markdown = ">=3.2.1" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +packaging = ">=20.5" +pathspec = ">=0.11.1" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4)", "ghp-import (==1.0)", "importlib-metadata (==4.3)", "jinja2 (==2.11.1)", "markdown (==3.2.1)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "packaging (==20.5)", "pathspec (==0.11.1)", "platformdirs (==2.2.0)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "typing-extensions (==3.10)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-autorefs" +version = "1.0.1" +description = "Automatically link across pages in MkDocs." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_autorefs-1.0.1-py3-none-any.whl", hash = "sha256:aacdfae1ab197780fb7a2dac92ad8a3d8f7ca8049a9cbe56a4218cd52e8da570"}, + {file = "mkdocs_autorefs-1.0.1.tar.gz", hash = "sha256:f684edf847eced40b570b57846b15f0bf57fb93ac2c510450775dcf16accb971"}, +] + +[package.dependencies] +Markdown = ">=3.3" +markupsafe = ">=2.0.1" +mkdocs = ">=1.1" + +[[package]] +name = "mkdocs-material" +version = "9.5.18" +description = "Documentation that simply works" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material-9.5.18-py3-none-any.whl", hash = "sha256:1e0e27fc9fe239f9064318acf548771a4629d5fd5dfd45444fd80a953fe21eb4"}, + {file = "mkdocs_material-9.5.18.tar.gz", hash = "sha256:a43f470947053fa2405c33995f282d24992c752a50114f23f30da9d8d0c57e62"}, +] + +[package.dependencies] +babel = ">=2.10,<3.0" +colorama = ">=0.4,<1.0" +jinja2 = ">=3.0,<4.0" +markdown = ">=3.2,<4.0" +mkdocs = ">=1.5.3,<1.6.0" +mkdocs-material-extensions = ">=1.3,<2.0" +paginate = ">=0.5,<1.0" +pygments = ">=2.16,<3.0" +pymdown-extensions = ">=10.2,<11.0" +regex = ">=2022.4" +requests = ">=2.26,<3.0" + +[package.extras] +git = ["mkdocs-git-committers-plugin-2 (>=1.1,<2.0)", "mkdocs-git-revision-date-localized-plugin (>=1.2.4,<2.0)"] +imaging = ["cairosvg (>=2.6,<3.0)", "pillow (>=10.2,<11.0)"] +recommended = ["mkdocs-minify-plugin (>=0.7,<1.0)", "mkdocs-redirects (>=1.2,<2.0)", "mkdocs-rss-plugin (>=1.6,<2.0)"] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +description = "Extension pack for Python Markdown and MkDocs Material." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31"}, + {file = "mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443"}, +] + +[[package]] +name = "mkdocstrings" +version = "0.22.0" +description = "Automatic documentation from sources, for MkDocs." +optional = false +python-versions = ">=3.7" +files = [ + {file = "mkdocstrings-0.22.0-py3-none-any.whl", hash = "sha256:2d4095d461554ff6a778fdabdca3c00c468c2f1459d469f7a7f622a2b23212ba"}, + {file = "mkdocstrings-0.22.0.tar.gz", hash = "sha256:82a33b94150ebb3d4b5c73bab4598c3e21468c79ec072eff6931c8f3bfc38256"}, +] + +[package.dependencies] +importlib-metadata = {version = ">=4.6", markers = "python_version < \"3.10\""} +Jinja2 = ">=2.11.1" +Markdown = ">=3.3" +MarkupSafe = ">=1.1" +mkdocs = ">=1.2" +mkdocs-autorefs = ">=0.3.1" +pymdown-extensions = ">=6.3" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.10\""} + +[package.extras] +crystal = ["mkdocstrings-crystal (>=0.3.4)"] +python = ["mkdocstrings-python (>=0.5.2)"] +python-legacy = ["mkdocstrings-python-legacy (>=0.2.1)"] + +[[package]] +name = "mkdocstrings-python" +version = "1.9.1" +description = "A Python handler for mkdocstrings." +optional = false +python-versions = ">=3.8" +files = [ + {file = "mkdocstrings_python-1.9.1-py3-none-any.whl", hash = "sha256:bf2406ed37ff19c9f8e0acc9d72c41953fb789bfb4ae10eb00ee17e537eeb220"}, + {file = "mkdocstrings_python-1.9.1.tar.gz", hash = "sha256:077188fa43eab3b689826b15da7da6753501224b2482e4eca3ce4412ce3b71cb"}, +] + +[package.dependencies] +griffe = ">=0.37" +markdown = ">=3.3,<3.6" +mkdocstrings = ">=0.20" + +[[package]] +name = "mypy" +version = "1.9.0" +description = "Optional static typing for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "mypy-1.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f8a67616990062232ee4c3952f41c779afac41405806042a8126fe96e098419f"}, + {file = "mypy-1.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d357423fa57a489e8c47b7c85dfb96698caba13d66e086b412298a1a0ea3b0ed"}, + {file = "mypy-1.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49c87c15aed320de9b438ae7b00c1ac91cd393c1b854c2ce538e2a72d55df150"}, + {file = "mypy-1.9.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:48533cdd345c3c2e5ef48ba3b0d3880b257b423e7995dada04248725c6f77374"}, + {file = "mypy-1.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:4d3dbd346cfec7cb98e6cbb6e0f3c23618af826316188d587d1c1bc34f0ede03"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:653265f9a2784db65bfca694d1edd23093ce49740b2244cde583aeb134c008f3"}, + {file = "mypy-1.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a3c007ff3ee90f69cf0a15cbcdf0995749569b86b6d2f327af01fd1b8aee9dc"}, + {file = "mypy-1.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2418488264eb41f69cc64a69a745fad4a8f86649af4b1041a4c64ee61fc61129"}, + {file = "mypy-1.9.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:68edad3dc7d70f2f17ae4c6c1b9471a56138ca22722487eebacfd1eb5321d612"}, + {file = "mypy-1.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:85ca5fcc24f0b4aeedc1d02f93707bccc04733f21d41c88334c5482219b1ccb3"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:aceb1db093b04db5cd390821464504111b8ec3e351eb85afd1433490163d60cd"}, + {file = "mypy-1.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0235391f1c6f6ce487b23b9dbd1327b4ec33bb93934aa986efe8a9563d9349e6"}, + {file = "mypy-1.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4d5ddc13421ba3e2e082a6c2d74c2ddb3979c39b582dacd53dd5d9431237185"}, + {file = "mypy-1.9.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:190da1ee69b427d7efa8aa0d5e5ccd67a4fb04038c380237a0d96829cb157913"}, + {file = "mypy-1.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:fe28657de3bfec596bbeef01cb219833ad9d38dd5393fc649f4b366840baefe6"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:e54396d70be04b34f31d2edf3362c1edd023246c82f1730bbf8768c28db5361b"}, + {file = "mypy-1.9.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:5e6061f44f2313b94f920e91b204ec600982961e07a17e0f6cd83371cb23f5c2"}, + {file = "mypy-1.9.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81a10926e5473c5fc3da8abb04119a1f5811a236dc3a38d92015cb1e6ba4cb9e"}, + {file = "mypy-1.9.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:b685154e22e4e9199fc95f298661deea28aaede5ae16ccc8cbb1045e716b3e04"}, + {file = "mypy-1.9.0-cp38-cp38-win_amd64.whl", hash = "sha256:5d741d3fc7c4da608764073089e5f58ef6352bedc223ff58f2f038c2c4698a89"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:587ce887f75dd9700252a3abbc9c97bbe165a4a630597845c61279cf32dfbf02"}, + {file = "mypy-1.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f88566144752999351725ac623471661c9d1cd8caa0134ff98cceeea181789f4"}, + {file = "mypy-1.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61758fabd58ce4b0720ae1e2fea5cfd4431591d6d590b197775329264f86311d"}, + {file = "mypy-1.9.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e49499be624dead83927e70c756970a0bc8240e9f769389cdf5714b0784ca6bf"}, + {file = "mypy-1.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:571741dc4194b4f82d344b15e8837e8c5fcc462d66d076748142327626a1b6e9"}, + {file = "mypy-1.9.0-py3-none-any.whl", hash = "sha256:a260627a570559181a9ea5de61ac6297aa5af202f06fd7ab093ce74e7181e43e"}, + {file = "mypy-1.9.0.tar.gz", hash = "sha256:3cc5da0127e6a478cddd906068496a97a7618a21ce9b54bde5bf7e539c7af974"}, +] + +[package.dependencies] +mypy-extensions = ">=1.0.0" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = ">=4.1.0" + +[package.extras] +dmypy = ["psutil (>=4.0)"] +install-types = ["pip"] +mypyc = ["setuptools (>=50)"] +reports = ["lxml"] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.0" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-24.0-py3-none-any.whl", hash = "sha256:2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5"}, + {file = "packaging-24.0.tar.gz", hash = "sha256:eb82c5e3e56209074766e6885bb04b8c38a0c015d0a30036ebe7ece34c9989e9"}, +] + +[[package]] +name = "paginate" +version = "0.5.6" +description = "Divides large result sets into pages for easier browsing" +optional = false +python-versions = "*" +files = [ + {file = "paginate-0.5.6.tar.gz", hash = "sha256:5e6007b6a9398177a7e1648d04fdd9f8c9766a1a945bceac82f1929e8c78af2d"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.0-py3-none-any.whl", hash = "sha256:0614df2a2f37e1a662acbd8e2b25b92ccf8632929bc6d43467e17fe89c75e068"}, + {file = "platformdirs-4.2.0.tar.gz", hash = "sha256:ef0cc731df711022c174543cb70a9b5bd22e5a9337c8624ef2c2ceb8ddad8768"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.7.0" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.7.0-py3-none-any.whl", hash = "sha256:9dee74a271705f14f9a1567671d144a851c675b072736f0a7b2608fd9e495352"}, + {file = "pydantic-2.7.0.tar.gz", hash = "sha256:b5ecdd42262ca2462e2624793551e80911a1e989f462910bb81aef974b4bb383"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.18.1" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.18.1" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:ee9cf33e7fe14243f5ca6977658eb7d1042caaa66847daacbd2117adb258b226"}, + {file = "pydantic_core-2.18.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6b7bbb97d82659ac8b37450c60ff2e9f97e4eb0f8a8a3645a5568b9334b08b50"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df4249b579e75094f7e9bb4bd28231acf55e308bf686b952f43100a5a0be394c"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d0491006a6ad20507aec2be72e7831a42efc93193d2402018007ff827dc62926"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:2ae80f72bb7a3e397ab37b53a2b49c62cc5496412e71bc4f1277620a7ce3f52b"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:58aca931bef83217fca7a390e0486ae327c4af9c3e941adb75f8772f8eeb03a1"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1be91ad664fc9245404a789d60cba1e91c26b1454ba136d2a1bf0c2ac0c0505a"}, + {file = "pydantic_core-2.18.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:667880321e916a8920ef49f5d50e7983792cf59f3b6079f3c9dac2b88a311d17"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:f7054fdc556f5421f01e39cbb767d5ec5c1139ea98c3e5b350e02e62201740c7"}, + {file = "pydantic_core-2.18.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:030e4f9516f9947f38179249778709a460a3adb516bf39b5eb9066fcfe43d0e6"}, + {file = "pydantic_core-2.18.1-cp310-none-win32.whl", hash = "sha256:2e91711e36e229978d92642bfc3546333a9127ecebb3f2761372e096395fc649"}, + {file = "pydantic_core-2.18.1-cp310-none-win_amd64.whl", hash = "sha256:9a29726f91c6cb390b3c2338f0df5cd3e216ad7a938762d11c994bb37552edb0"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9ece8a49696669d483d206b4474c367852c44815fca23ac4e48b72b339807f80"}, + {file = "pydantic_core-2.18.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a5d83efc109ceddb99abd2c1316298ced2adb4570410defe766851a804fcd5b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f7973c381283783cd1043a8c8f61ea5ce7a3a58b0369f0ee0ee975eaf2f2a1b"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:54c7375c62190a7845091f521add19b0f026bcf6ae674bdb89f296972272e86d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dd63cec4e26e790b70544ae5cc48d11b515b09e05fdd5eff12e3195f54b8a586"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:561cf62c8a3498406495cfc49eee086ed2bb186d08bcc65812b75fda42c38294"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68717c38a68e37af87c4da20e08f3e27d7e4212e99e96c3d875fbf3f4812abfc"}, + {file = "pydantic_core-2.18.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2d5728e93d28a3c63ee513d9ffbac9c5989de8c76e049dbcb5bfe4b923a9739d"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:f0f17814c505f07806e22b28856c59ac80cee7dd0fbb152aed273e116378f519"}, + {file = "pydantic_core-2.18.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d816f44a51ba5175394bc6c7879ca0bd2be560b2c9e9f3411ef3a4cbe644c2e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win32.whl", hash = "sha256:09f03dfc0ef8c22622eaa8608caa4a1e189cfb83ce847045eca34f690895eccb"}, + {file = "pydantic_core-2.18.1-cp311-none-win_amd64.whl", hash = "sha256:27f1009dc292f3b7ca77feb3571c537276b9aad5dd4efb471ac88a8bd09024e9"}, + {file = "pydantic_core-2.18.1-cp311-none-win_arm64.whl", hash = "sha256:48dd883db92e92519201f2b01cafa881e5f7125666141a49ffba8b9facc072b0"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b6b0e4912030c6f28bcb72b9ebe4989d6dc2eebcd2a9cdc35fefc38052dd4fe8"}, + {file = "pydantic_core-2.18.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f3202a429fe825b699c57892d4371c74cc3456d8d71b7f35d6028c96dfecad31"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a3982b0a32d0a88b3907e4b0dc36809fda477f0757c59a505d4e9b455f384b8b"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25595ac311f20e5324d1941909b0d12933f1fd2171075fcff763e90f43e92a0d"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:14fe73881cf8e4cbdaded8ca0aa671635b597e42447fec7060d0868b52d074e6"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ca976884ce34070799e4dfc6fbd68cb1d181db1eefe4a3a94798ddfb34b8867f"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:684d840d2c9ec5de9cb397fcb3f36d5ebb6fa0d94734f9886032dd796c1ead06"}, + {file = "pydantic_core-2.18.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:54764c083bbe0264f0f746cefcded6cb08fbbaaf1ad1d78fb8a4c30cff999a90"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:201713f2f462e5c015b343e86e68bd8a530a4f76609b33d8f0ec65d2b921712a"}, + {file = "pydantic_core-2.18.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fd1a9edb9dd9d79fbeac1ea1f9a8dd527a6113b18d2e9bcc0d541d308dae639b"}, + {file = "pydantic_core-2.18.1-cp312-none-win32.whl", hash = "sha256:d5e6b7155b8197b329dc787356cfd2684c9d6a6b1a197f6bbf45f5555a98d411"}, + {file = "pydantic_core-2.18.1-cp312-none-win_amd64.whl", hash = "sha256:9376d83d686ec62e8b19c0ac3bf8d28d8a5981d0df290196fb6ef24d8a26f0d6"}, + {file = "pydantic_core-2.18.1-cp312-none-win_arm64.whl", hash = "sha256:c562b49c96906b4029b5685075fe1ebd3b5cc2601dfa0b9e16c2c09d6cbce048"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:3e352f0191d99fe617371096845070dee295444979efb8f27ad941227de6ad09"}, + {file = "pydantic_core-2.18.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:c0295d52b012cbe0d3059b1dba99159c3be55e632aae1999ab74ae2bd86a33d7"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:56823a92075780582d1ffd4489a2e61d56fd3ebb4b40b713d63f96dd92d28144"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:dd3f79e17b56741b5177bcc36307750d50ea0698df6aa82f69c7db32d968c1c2"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:38a5024de321d672a132b1834a66eeb7931959c59964b777e8f32dbe9523f6b1"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2ce426ee691319d4767748c8e0895cfc56593d725594e415f274059bcf3cb76"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2adaeea59849ec0939af5c5d476935f2bab4b7f0335b0110f0f069a41024278e"}, + {file = "pydantic_core-2.18.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9b6431559676a1079eac0f52d6d0721fb8e3c5ba43c37bc537c8c83724031feb"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:85233abb44bc18d16e72dc05bf13848a36f363f83757541f1a97db2f8d58cfd9"}, + {file = "pydantic_core-2.18.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:641a018af4fe48be57a2b3d7a1f0f5dbca07c1d00951d3d7463f0ac9dac66622"}, + {file = "pydantic_core-2.18.1-cp38-none-win32.whl", hash = "sha256:63d7523cd95d2fde0d28dc42968ac731b5bb1e516cc56b93a50ab293f4daeaad"}, + {file = "pydantic_core-2.18.1-cp38-none-win_amd64.whl", hash = "sha256:907a4d7720abfcb1c81619863efd47c8a85d26a257a2dbebdb87c3b847df0278"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:aad17e462f42ddbef5984d70c40bfc4146c322a2da79715932cd8976317054de"}, + {file = "pydantic_core-2.18.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:94b9769ba435b598b547c762184bcfc4783d0d4c7771b04a3b45775c3589ca44"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:80e0e57cc704a52fb1b48f16d5b2c8818da087dbee6f98d9bf19546930dc64b5"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:76b86e24039c35280ceee6dce7e62945eb93a5175d43689ba98360ab31eebc4a"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a05db5013ec0ca4a32cc6433f53faa2a014ec364031408540ba858c2172bb0"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:250ae39445cb5475e483a36b1061af1bc233de3e9ad0f4f76a71b66231b07f88"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a32204489259786a923e02990249c65b0f17235073149d0033efcebe80095570"}, + {file = "pydantic_core-2.18.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6395a4435fa26519fd96fdccb77e9d00ddae9dd6c742309bd0b5610609ad7fb2"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:2533ad2883f001efa72f3d0e733fb846710c3af6dcdd544fe5bf14fa5fe2d7db"}, + {file = "pydantic_core-2.18.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:b560b72ed4816aee52783c66854d96157fd8175631f01ef58e894cc57c84f0f6"}, + {file = "pydantic_core-2.18.1-cp39-none-win32.whl", hash = "sha256:582cf2cead97c9e382a7f4d3b744cf0ef1a6e815e44d3aa81af3ad98762f5a9b"}, + {file = "pydantic_core-2.18.1-cp39-none-win_amd64.whl", hash = "sha256:ca71d501629d1fa50ea7fa3b08ba884fe10cefc559f5c6c8dfe9036c16e8ae89"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:e178e5b66a06ec5bf51668ec0d4ac8cfb2bdcb553b2c207d58148340efd00143"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:72722ce529a76a4637a60be18bd789d8fb871e84472490ed7ddff62d5fed620d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2fe0c1ce5b129455e43f941f7a46f61f3d3861e571f2905d55cdbb8b5c6f5e2c"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d4284c621f06a72ce2cb55f74ea3150113d926a6eb78ab38340c08f770eb9b4d"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a0c3e718f4e064efde68092d9d974e39572c14e56726ecfaeebbe6544521f47"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2027493cc44c23b598cfaf200936110433d9caa84e2c6cf487a83999638a96ac"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:76909849d1a6bffa5a07742294f3fa1d357dc917cb1fe7b470afbc3a7579d539"}, + {file = "pydantic_core-2.18.1-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ee7ccc7fb7e921d767f853b47814c3048c7de536663e82fbc37f5eb0d532224b"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ee2794111c188548a4547eccc73a6a8527fe2af6cf25e1a4ebda2fd01cdd2e60"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a139fe9f298dc097349fb4f28c8b81cc7a202dbfba66af0e14be5cfca4ef7ce5"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d074b07a10c391fc5bbdcb37b2f16f20fcd9e51e10d01652ab298c0d07908ee2"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c69567ddbac186e8c0aadc1f324a60a564cfe25e43ef2ce81bcc4b8c3abffbae"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:baf1c7b78cddb5af00971ad5294a4583188bda1495b13760d9f03c9483bb6203"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:2684a94fdfd1b146ff10689c6e4e815f6a01141781c493b97342cdc5b06f4d5d"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:73c1bc8a86a5c9e8721a088df234265317692d0b5cd9e86e975ce3bc3db62a59"}, + {file = "pydantic_core-2.18.1-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:e60defc3c15defb70bb38dd605ff7e0fae5f6c9c7cbfe0ad7868582cb7e844a6"}, + {file = "pydantic_core-2.18.1.tar.gz", hash = "sha256:de9d3e8717560eb05e28739d1b35e4eac2e458553a52a301e51352a7ffc86a35"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pygments" +version = "2.17.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pygments-2.17.2-py3-none-any.whl", hash = "sha256:b27c2826c47d0f3219f29554824c30c5e8945175d888647acd804ddd04af846c"}, + {file = "pygments-2.17.2.tar.gz", hash = "sha256:da46cec9fd2de5be3a8a784f434e4c4ab670b4ff54d605c4c2717e9d49c4c367"}, +] + +[package.extras] +plugins = ["importlib-metadata"] +windows-terminal = ["colorama (>=0.4.6)"] + +[[package]] +name = "pymdown-extensions" +version = "10.7.1" +description = "Extension pack for Python Markdown." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pymdown_extensions-10.7.1-py3-none-any.whl", hash = "sha256:f5cc7000d7ff0d1ce9395d216017fa4df3dde800afb1fb72d1c7d3fd35e710f4"}, + {file = "pymdown_extensions-10.7.1.tar.gz", hash = "sha256:c70e146bdd83c744ffc766b4671999796aba18842b268510a329f7f64700d584"}, +] + +[package.dependencies] +markdown = ">=3.5" +pyyaml = "*" + +[package.extras] +extra = ["pygments (>=2.12)"] + +[[package]] +name = "pyparsing" +version = "3.1.2" +description = "pyparsing module - Classes and methods to define and execute parsing grammars" +optional = false +python-versions = ">=3.6.8" +files = [ + {file = "pyparsing-3.1.2-py3-none-any.whl", hash = "sha256:f9db75911801ed778fe61bb643079ff86601aca99fcae6345aa67292038fb742"}, + {file = "pyparsing-3.1.2.tar.gz", hash = "sha256:a1bac0ce561155ecc3ed78ca94d3c9378656ad4c94c1270de543f621420f94ad"}, +] + +[package.extras] +diagrams = ["jinja2", "railroad-diagrams"] + +[[package]] +name = "pytest" +version = "7.4.4" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-7.4.4-py3-none-any.whl", hash = "sha256:b090cdf5ed60bf4c45261be03239c2c1c22df034fbffe691abe93cd80cea01d8"}, + {file = "pytest-7.4.4.tar.gz", hash = "sha256:2cf0005922c6ace4a3e2ec8b4080eb0d9753fdc93107415332f50ce9e7994280"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=0.12,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-asyncio" +version = "0.23.6" +description = "Pytest support for asyncio" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-asyncio-0.23.6.tar.gz", hash = "sha256:ffe523a89c1c222598c76856e76852b787504ddb72dd5d9b6617ffa8aa2cde5f"}, + {file = "pytest_asyncio-0.23.6-py3-none-any.whl", hash = "sha256:68516fdd1018ac57b846c9846b954f0393b26f094764a28c955eabb0536a4e8a"}, +] + +[package.dependencies] +pytest = ">=7.0.0,<9" + +[package.extras] +docs = ["sphinx (>=5.3)", "sphinx-rtd-theme (>=1.0)"] +testing = ["coverage (>=6.2)", "hypothesis (>=5.7.1)"] + +[[package]] +name = "pytest-cov" +version = "4.1.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest-cov-4.1.0.tar.gz", hash = "sha256:3904b13dfbfec47f003b8e77fd5b589cd11904a21ddf1ab38a64f204d6a10ef6"}, + {file = "pytest_cov-4.1.0-py3-none-any.whl", hash = "sha256:6ba70b9e97e69fcc3fb45bfeab2d0a138fb65c4d0d6a41ef33983ad114be8c3a"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "six", "virtualenv"] + +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "pyyaml-env-tag" +version = "0.1" +description = "A custom YAML tag for referencing environment variables in YAML files. " +optional = false +python-versions = ">=3.6" +files = [ + {file = "pyyaml_env_tag-0.1-py3-none-any.whl", hash = "sha256:af31106dec8a4d68c60207c1886031cbf839b68aa7abccdb19868200532c2069"}, + {file = "pyyaml_env_tag-0.1.tar.gz", hash = "sha256:70092675bda14fdec33b31ba77e7543de9ddc88f2e5b99160396572d11525bdb"}, +] + +[package.dependencies] +pyyaml = "*" + +[[package]] +name = "regex" +version = "2024.4.16" +description = "Alternative regular expression module, to replace re." +optional = false +python-versions = ">=3.7" +files = [ + {file = "regex-2024.4.16-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:fb83cc090eac63c006871fd24db5e30a1f282faa46328572661c0a24a2323a08"}, + {file = "regex-2024.4.16-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8c91e1763696c0eb66340c4df98623c2d4e77d0746b8f8f2bee2c6883fd1fe18"}, + {file = "regex-2024.4.16-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:10188fe732dec829c7acca7422cdd1bf57d853c7199d5a9e96bb4d40db239c73"}, + {file = "regex-2024.4.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:956b58d692f235cfbf5b4f3abd6d99bf102f161ccfe20d2fd0904f51c72c4c66"}, + {file = "regex-2024.4.16-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a70b51f55fd954d1f194271695821dd62054d949efd6368d8be64edd37f55c86"}, + {file = "regex-2024.4.16-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c02fcd2bf45162280613d2e4a1ca3ac558ff921ae4e308ecb307650d3a6ee51"}, + {file = "regex-2024.4.16-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c4ed75ea6892a56896d78f11006161eea52c45a14994794bcfa1654430984b22"}, + {file = "regex-2024.4.16-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd727ad276bb91928879f3aa6396c9a1d34e5e180dce40578421a691eeb77f47"}, + {file = "regex-2024.4.16-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:7cbc5d9e8a1781e7be17da67b92580d6ce4dcef5819c1b1b89f49d9678cc278c"}, + {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:78fddb22b9ef810b63ef341c9fcf6455232d97cfe03938cbc29e2672c436670e"}, + {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:445ca8d3c5a01309633a0c9db57150312a181146315693273e35d936472df912"}, + {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:95399831a206211d6bc40224af1c635cb8790ddd5c7493e0bd03b85711076a53"}, + {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:7731728b6568fc286d86745f27f07266de49603a6fdc4d19c87e8c247be452af"}, + {file = "regex-2024.4.16-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4facc913e10bdba42ec0aee76d029aedda628161a7ce4116b16680a0413f658a"}, + {file = "regex-2024.4.16-cp310-cp310-win32.whl", hash = "sha256:911742856ce98d879acbea33fcc03c1d8dc1106234c5e7d068932c945db209c0"}, + {file = "regex-2024.4.16-cp310-cp310-win_amd64.whl", hash = "sha256:e0a2df336d1135a0b3a67f3bbf78a75f69562c1199ed9935372b82215cddd6e2"}, + {file = "regex-2024.4.16-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1210365faba7c2150451eb78ec5687871c796b0f1fa701bfd2a4a25420482d26"}, + {file = "regex-2024.4.16-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:9ab40412f8cd6f615bfedea40c8bf0407d41bf83b96f6fc9ff34976d6b7037fd"}, + {file = "regex-2024.4.16-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fd80d1280d473500d8086d104962a82d77bfbf2b118053824b7be28cd5a79ea5"}, + {file = "regex-2024.4.16-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bb966fdd9217e53abf824f437a5a2d643a38d4fd5fd0ca711b9da683d452969"}, + {file = "regex-2024.4.16-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:20b7a68444f536365af42a75ccecb7ab41a896a04acf58432db9e206f4e525d6"}, + {file = "regex-2024.4.16-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b74586dd0b039c62416034f811d7ee62810174bb70dffcca6439f5236249eb09"}, + {file = "regex-2024.4.16-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c8290b44d8b0af4e77048646c10c6e3aa583c1ca67f3b5ffb6e06cf0c6f0f89"}, + {file = "regex-2024.4.16-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2d80a6749724b37853ece57988b39c4e79d2b5fe2869a86e8aeae3bbeef9eb0"}, + {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:3a1018e97aeb24e4f939afcd88211ace472ba566efc5bdf53fd8fd7f41fa7170"}, + {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:8d015604ee6204e76569d2f44e5a210728fa917115bef0d102f4107e622b08d5"}, + {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:3d5ac5234fb5053850d79dd8eb1015cb0d7d9ed951fa37aa9e6249a19aa4f336"}, + {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:0a38d151e2cdd66d16dab550c22f9521ba79761423b87c01dae0a6e9add79c0d"}, + {file = "regex-2024.4.16-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:159dc4e59a159cb8e4e8f8961eb1fa5d58f93cb1acd1701d8aff38d45e1a84a6"}, + {file = "regex-2024.4.16-cp311-cp311-win32.whl", hash = "sha256:ba2336d6548dee3117520545cfe44dc28a250aa091f8281d28804aa8d707d93d"}, + {file = "regex-2024.4.16-cp311-cp311-win_amd64.whl", hash = "sha256:8f83b6fd3dc3ba94d2b22717f9c8b8512354fd95221ac661784df2769ea9bba9"}, + {file = "regex-2024.4.16-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:80b696e8972b81edf0af2a259e1b2a4a661f818fae22e5fa4fa1a995fb4a40fd"}, + {file = "regex-2024.4.16-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d61ae114d2a2311f61d90c2ef1358518e8f05eafda76eaf9c772a077e0b465ec"}, + {file = "regex-2024.4.16-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8ba6745440b9a27336443b0c285d705ce73adb9ec90e2f2004c64d95ab5a7598"}, + {file = "regex-2024.4.16-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6295004b2dd37b0835ea5c14a33e00e8cfa3c4add4d587b77287825f3418d310"}, + {file = "regex-2024.4.16-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4aba818dcc7263852aabb172ec27b71d2abca02a593b95fa79351b2774eb1d2b"}, + {file = "regex-2024.4.16-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d0800631e565c47520aaa04ae38b96abc5196fe8b4aa9bd864445bd2b5848a7a"}, + {file = "regex-2024.4.16-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:08dea89f859c3df48a440dbdcd7b7155bc675f2fa2ec8c521d02dc69e877db70"}, + {file = "regex-2024.4.16-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eeaa0b5328b785abc344acc6241cffde50dc394a0644a968add75fcefe15b9d4"}, + {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4e819a806420bc010489f4e741b3036071aba209f2e0989d4750b08b12a9343f"}, + {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c2d0e7cbb6341e830adcbfa2479fdeebbfbb328f11edd6b5675674e7a1e37730"}, + {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:91797b98f5e34b6a49f54be33f72e2fb658018ae532be2f79f7c63b4ae225145"}, + {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:d2da13568eff02b30fd54fccd1e042a70fe920d816616fda4bf54ec705668d81"}, + {file = "regex-2024.4.16-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:370c68dc5570b394cbaadff50e64d705f64debed30573e5c313c360689b6aadc"}, + {file = "regex-2024.4.16-cp312-cp312-win32.whl", hash = "sha256:904c883cf10a975b02ab3478bce652f0f5346a2c28d0a8521d97bb23c323cc8b"}, + {file = "regex-2024.4.16-cp312-cp312-win_amd64.whl", hash = "sha256:785c071c982dce54d44ea0b79cd6dfafddeccdd98cfa5f7b86ef69b381b457d9"}, + {file = "regex-2024.4.16-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:e2f142b45c6fed48166faeb4303b4b58c9fcd827da63f4cf0a123c3480ae11fb"}, + {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e87ab229332ceb127a165612d839ab87795972102cb9830e5f12b8c9a5c1b508"}, + {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:81500ed5af2090b4a9157a59dbc89873a25c33db1bb9a8cf123837dcc9765047"}, + {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b340cccad138ecb363324aa26893963dcabb02bb25e440ebdf42e30963f1a4e0"}, + {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c72608e70f053643437bd2be0608f7f1c46d4022e4104d76826f0839199347a"}, + {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a01fe2305e6232ef3e8f40bfc0f0f3a04def9aab514910fa4203bafbc0bb4682"}, + {file = "regex-2024.4.16-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:03576e3a423d19dda13e55598f0fd507b5d660d42c51b02df4e0d97824fdcae3"}, + {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:549c3584993772e25f02d0656ac48abdda73169fe347263948cf2b1cead622f3"}, + {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:34422d5a69a60b7e9a07a690094e824b66f5ddc662a5fc600d65b7c174a05f04"}, + {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:5f580c651a72b75c39e311343fe6875d6f58cf51c471a97f15a938d9fe4e0d37"}, + {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:3399dd8a7495bbb2bacd59b84840eef9057826c664472e86c91d675d007137f5"}, + {file = "regex-2024.4.16-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8d1f86f3f4e2388aa3310b50694ac44daefbd1681def26b4519bd050a398dc5a"}, + {file = "regex-2024.4.16-cp37-cp37m-win32.whl", hash = "sha256:dd5acc0a7d38fdc7a3a6fd3ad14c880819008ecb3379626e56b163165162cc46"}, + {file = "regex-2024.4.16-cp37-cp37m-win_amd64.whl", hash = "sha256:ba8122e3bb94ecda29a8de4cf889f600171424ea586847aa92c334772d200331"}, + {file = "regex-2024.4.16-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:743deffdf3b3481da32e8a96887e2aa945ec6685af1cfe2bcc292638c9ba2f48"}, + {file = "regex-2024.4.16-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:7571f19f4a3fd00af9341c7801d1ad1967fc9c3f5e62402683047e7166b9f2b4"}, + {file = "regex-2024.4.16-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:df79012ebf6f4efb8d307b1328226aef24ca446b3ff8d0e30202d7ebcb977a8c"}, + {file = "regex-2024.4.16-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e757d475953269fbf4b441207bb7dbdd1c43180711b6208e129b637792ac0b93"}, + {file = "regex-2024.4.16-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4313ab9bf6a81206c8ac28fdfcddc0435299dc88cad12cc6305fd0e78b81f9e4"}, + {file = "regex-2024.4.16-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d83c2bc678453646f1a18f8db1e927a2d3f4935031b9ad8a76e56760461105dd"}, + {file = "regex-2024.4.16-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9df1bfef97db938469ef0a7354b2d591a2d438bc497b2c489471bec0e6baf7c4"}, + {file = "regex-2024.4.16-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62120ed0de69b3649cc68e2965376048793f466c5a6c4370fb27c16c1beac22d"}, + {file = "regex-2024.4.16-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:c2ef6f7990b6e8758fe48ad08f7e2f66c8f11dc66e24093304b87cae9037bb4a"}, + {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:8fc6976a3395fe4d1fbeb984adaa8ec652a1e12f36b56ec8c236e5117b585427"}, + {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:03e68f44340528111067cecf12721c3df4811c67268b897fbe695c95f860ac42"}, + {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:ec7e0043b91115f427998febaa2beb82c82df708168b35ece3accb610b91fac1"}, + {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:c21fc21a4c7480479d12fd8e679b699f744f76bb05f53a1d14182b31f55aac76"}, + {file = "regex-2024.4.16-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:12f6a3f2f58bb7344751919a1876ee1b976fe08b9ffccb4bbea66f26af6017b9"}, + {file = "regex-2024.4.16-cp38-cp38-win32.whl", hash = "sha256:479595a4fbe9ed8f8f72c59717e8cf222da2e4c07b6ae5b65411e6302af9708e"}, + {file = "regex-2024.4.16-cp38-cp38-win_amd64.whl", hash = "sha256:0534b034fba6101611968fae8e856c1698da97ce2efb5c2b895fc8b9e23a5834"}, + {file = "regex-2024.4.16-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a7ccdd1c4a3472a7533b0a7aa9ee34c9a2bef859ba86deec07aff2ad7e0c3b94"}, + {file = "regex-2024.4.16-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6f2f017c5be19984fbbf55f8af6caba25e62c71293213f044da3ada7091a4455"}, + {file = "regex-2024.4.16-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:803b8905b52de78b173d3c1e83df0efb929621e7b7c5766c0843704d5332682f"}, + {file = "regex-2024.4.16-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:684008ec44ad275832a5a152f6e764bbe1914bea10968017b6feaecdad5736e0"}, + {file = "regex-2024.4.16-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65436dce9fdc0aeeb0a0effe0839cb3d6a05f45aa45a4d9f9c60989beca78b9c"}, + {file = "regex-2024.4.16-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea355eb43b11764cf799dda62c658c4d2fdb16af41f59bb1ccfec517b60bcb07"}, + {file = "regex-2024.4.16-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:98c1165f3809ce7774f05cb74e5408cd3aa93ee8573ae959a97a53db3ca3180d"}, + {file = "regex-2024.4.16-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cccc79a9be9b64c881f18305a7c715ba199e471a3973faeb7ba84172abb3f317"}, + {file = "regex-2024.4.16-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:00169caa125f35d1bca6045d65a662af0202704489fada95346cfa092ec23f39"}, + {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:6cc38067209354e16c5609b66285af17a2863a47585bcf75285cab33d4c3b8df"}, + {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:23cff1b267038501b179ccbbd74a821ac4a7192a1852d1d558e562b507d46013"}, + {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:b9d320b3bf82a39f248769fc7f188e00f93526cc0fe739cfa197868633d44701"}, + {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:89ec7f2c08937421bbbb8b48c54096fa4f88347946d4747021ad85f1b3021b3c"}, + {file = "regex-2024.4.16-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:4918fd5f8b43aa7ec031e0fef1ee02deb80b6afd49c85f0790be1dc4ce34cb50"}, + {file = "regex-2024.4.16-cp39-cp39-win32.whl", hash = "sha256:684e52023aec43bdf0250e843e1fdd6febbe831bd9d52da72333fa201aaa2335"}, + {file = "regex-2024.4.16-cp39-cp39-win_amd64.whl", hash = "sha256:e697e1c0238133589e00c244a8b676bc2cfc3ab4961318d902040d099fec7483"}, + {file = "regex-2024.4.16.tar.gz", hash = "sha256:fa454d26f2e87ad661c4f0c5a5fe4cf6aab1e307d1b94f16ffdfcb089ba685c0"}, +] + +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "rich" +version = "13.7.1" +description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "rich-13.7.1-py3-none-any.whl", hash = "sha256:4edbae314f59eb482f54e9e30bf00d33350aaa94f4bfcd4e9e3110e64d0d7222"}, + {file = "rich-13.7.1.tar.gz", hash = "sha256:9be308cb1fe2f1f57d67ce99e95af38a1e2bc71ad9813b0e247cf7ffbcc3a432"}, +] + +[package.dependencies] +markdown-it-py = ">=2.2.0" +pygments = ">=2.13.0,<3.0.0" + +[package.extras] +jupyter = ["ipywidgets (>=7.5.1,<9)"] + +[[package]] +name = "ruff" +version = "0.1.15" +description = "An extremely fast Python linter and code formatter, written in Rust." +optional = false +python-versions = ">=3.7" +files = [ + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:5fe8d54df166ecc24106db7dd6a68d44852d14eb0729ea4672bb4d96c320b7df"}, + {file = "ruff-0.1.15-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6f0bfbb53c4b4de117ac4d6ddfd33aa5fc31beeaa21d23c45c6dd249faf9126f"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e0d432aec35bfc0d800d4f70eba26e23a352386be3a6cf157083d18f6f5881c8"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9405fa9ac0e97f35aaddf185a1be194a589424b8713e3b97b762336ec79ff807"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c66ec24fe36841636e814b8f90f572a8c0cb0e54d8b5c2d0e300d28a0d7bffec"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:6f8ad828f01e8dd32cc58bc28375150171d198491fc901f6f98d2a39ba8e3ff5"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86811954eec63e9ea162af0ffa9f8d09088bab51b7438e8b6488b9401863c25e"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd4025ac5e87d9b80e1f300207eb2fd099ff8200fa2320d7dc066a3f4622dc6b"}, + {file = "ruff-0.1.15-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b17b93c02cdb6aeb696effecea1095ac93f3884a49a554a9afa76bb125c114c1"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:ddb87643be40f034e97e97f5bc2ef7ce39de20e34608f3f829db727a93fb82c5"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:abf4822129ed3a5ce54383d5f0e964e7fef74a41e48eb1dfad404151efc130a2"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6c629cf64bacfd136c07c78ac10a54578ec9d1bd2a9d395efbee0935868bf852"}, + {file = "ruff-0.1.15-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1bab866aafb53da39c2cadfb8e1c4550ac5340bb40300083eb8967ba25481447"}, + {file = "ruff-0.1.15-py3-none-win32.whl", hash = "sha256:2417e1cb6e2068389b07e6fa74c306b2810fe3ee3476d5b8a96616633f40d14f"}, + {file = "ruff-0.1.15-py3-none-win_amd64.whl", hash = "sha256:3837ac73d869efc4182d9036b1405ef4c73d9b1f88da2413875e34e0d6919587"}, + {file = "ruff-0.1.15-py3-none-win_arm64.whl", hash = "sha256:9a933dfb1c14ec7a33cceb1e49ec4a16b51ce3c20fd42663198746efc0427360"}, + {file = "ruff-0.1.15.tar.gz", hash = "sha256:f6dfa8c1b21c913c326919056c390966648b680966febcb796cc9d1aaab8564e"}, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +description = "Tool to Detect Surrounding Shell" +optional = false +python-versions = ">=3.7" +files = [ + {file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"}, + {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, +] + +[[package]] +name = "six" +version = "1.16.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*" +files = [ + {file = "six-1.16.0-py2.py3-none-any.whl", hash = "sha256:8abb2f1d86890a2dfb989f9a77cfcfd3e47c2a354b01111771326f8aa26e0254"}, + {file = "six-1.16.0.tar.gz", hash = "sha256:1e61c37477a1626458e36f7b1d82aa5c9b094fa4802892072e49de9c60c4c926"}, +] + +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + +[[package]] +name = "typer" +version = "0.12.3" +description = "Typer, build great CLIs. Easy to code. Based on Python type hints." +optional = false +python-versions = ">=3.7" +files = [ + {file = "typer-0.12.3-py3-none-any.whl", hash = "sha256:070d7ca53f785acbccba8e7d28b08dcd88f79f1fbda035ade0aecec71ca5c914"}, + {file = "typer-0.12.3.tar.gz", hash = "sha256:49e73131481d804288ef62598d97a1ceef3058905aa536a1134f90891ba35482"}, +] + +[package.dependencies] +click = ">=8.0.0" +rich = ">=10.11.0" +shellingham = ">=1.3.0" +typing-extensions = ">=3.7.4.3" + +[[package]] +name = "typing-extensions" +version = "4.11.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.11.0-py3-none-any.whl", hash = "sha256:c1f94d72897edaf4ce775bb7558d5b79d8126906a14ea5ed1635921406c0387a"}, + {file = "typing_extensions-4.11.0.tar.gz", hash = "sha256:83f085bd5ca59c80295fc2a82ab5dac679cbe02b9f33f7d83af68e241bea51b0"}, +] + +[[package]] +name = "urllib3" +version = "2.2.1" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.1-py3-none-any.whl", hash = "sha256:450b20ec296a467077128bff42b73080516e71b56ff59a60a02bef2232c4fa9d"}, + {file = "urllib3-2.2.1.tar.gz", hash = "sha256:d0570876c61ab9e520d776c38acbbb5b05a776d3f9ff98a5c8fd5162a444cf19"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + +[[package]] +name = "verspec" +version = "0.1.0" +description = "Flexible version handling" +optional = false +python-versions = "*" +files = [ + {file = "verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31"}, + {file = "verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e"}, +] + +[package.extras] +test = ["coverage", "flake8 (>=3.7)", "mypy", "pretend", "pytest"] + +[[package]] +name = "watchdog" +version = "4.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:39cb34b1f1afbf23e9562501673e7146777efe95da24fab5707b88f7fb11649b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c522392acc5e962bcac3b22b9592493ffd06d1fc5d755954e6be9f4990de932b"}, + {file = "watchdog-4.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:6c47bdd680009b11c9ac382163e05ca43baf4127954c5f6d0250e7d772d2b80c"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8350d4055505412a426b6ad8c521bc7d367d1637a762c70fdd93a3a0d595990b"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c17d98799f32e3f55f181f19dd2021d762eb38fdd381b4a748b9f5a36738e935"}, + {file = "watchdog-4.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4986db5e8880b0e6b7cd52ba36255d4793bf5cdc95bd6264806c233173b1ec0b"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:11e12fafb13372e18ca1bbf12d50f593e7280646687463dd47730fd4f4d5d257"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:5369136a6474678e02426bd984466343924d1df8e2fd94a9b443cb7e3aa20d19"}, + {file = "watchdog-4.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:76ad8484379695f3fe46228962017a7e1337e9acadafed67eb20aabb175df98b"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:45cc09cc4c3b43fb10b59ef4d07318d9a3ecdbff03abd2e36e77b6dd9f9a5c85"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:eed82cdf79cd7f0232e2fdc1ad05b06a5e102a43e331f7d041e5f0e0a34a51c4"}, + {file = "watchdog-4.0.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:ba30a896166f0fee83183cec913298151b73164160d965af2e93a20bbd2ab605"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:d18d7f18a47de6863cd480734613502904611730f8def45fc52a5d97503e5101"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:2895bf0518361a9728773083908801a376743bcc37dfa252b801af8fd281b1ca"}, + {file = "watchdog-4.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:87e9df830022488e235dd601478c15ad73a0389628588ba0b028cb74eb72fed8"}, + {file = "watchdog-4.0.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6e949a8a94186bced05b6508faa61b7adacc911115664ccb1923b9ad1f1ccf7b"}, + {file = "watchdog-4.0.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:6a4db54edea37d1058b08947c789a2354ee02972ed5d1e0dca9b0b820f4c7f92"}, + {file = "watchdog-4.0.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:d31481ccf4694a8416b681544c23bd271f5a123162ab603c7d7d2dd7dd901a07"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:8fec441f5adcf81dd240a5fe78e3d83767999771630b5ddfc5867827a34fa3d3"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:6a9c71a0b02985b4b0b6d14b875a6c86ddea2fdbebd0c9a720a806a8bbffc69f"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:557ba04c816d23ce98a06e70af6abaa0485f6d94994ec78a42b05d1c03dcbd50"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:d0f9bd1fd919134d459d8abf954f63886745f4660ef66480b9d753a7c9d40927"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:f9b2fdca47dc855516b2d66eef3c39f2672cbf7e7a42e7e67ad2cbfcd6ba107d"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:73c7a935e62033bd5e8f0da33a4dcb763da2361921a69a5a95aaf6c93aa03a87"}, + {file = "watchdog-4.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6a80d5cae8c265842c7419c560b9961561556c4361b297b4c431903f8c33b269"}, + {file = "watchdog-4.0.0-py3-none-win32.whl", hash = "sha256:8f9a542c979df62098ae9c58b19e03ad3df1c9d8c6895d96c0d51da17b243b1c"}, + {file = "watchdog-4.0.0-py3-none-win_amd64.whl", hash = "sha256:f970663fa4f7e80401a7b0cbeec00fa801bf0287d93d48368fc3e6fa32716245"}, + {file = "watchdog-4.0.0-py3-none-win_ia64.whl", hash = "sha256:9a03e16e55465177d416699331b0f3564138f1807ecc5f2de9d55d8f188d08c7"}, + {file = "watchdog-4.0.0.tar.gz", hash = "sha256:e3e7065cbdabe6183ab82199d7a4f6b3ba0a438c5a512a68559846ccb76a78ec"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + +[[package]] +name = "zipp" +version = "3.18.1" +description = "Backport of pathlib-compatible object wrapper for zip files" +optional = false +python-versions = ">=3.8" +files = [ + {file = "zipp-3.18.1-py3-none-any.whl", hash = "sha256:206f5a15f2af3dbaee80769fb7dc6f249695e940acca08dfb2a4769fe61e538b"}, + {file = "zipp-3.18.1.tar.gz", hash = "sha256:2884ed22e7d8961de1c9a05142eb69a247f120291bc0206a00a7642f09b5b715"}, +] + +[package.extras] +docs = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] +testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=2.2)", "pytest-ignore-flaky", "pytest-mypy", "pytest-ruff (>=0.2.1)"] + +[metadata] +lock-version = "2.0" +python-versions = ">=3.9,<3.13" +content-hash = "5bd3e9545ee327a4454e523174d3aabf1b9cfa6341be1860caa6d7e2cba1c930" diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..bd7f7c6 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,88 @@ +[tool.poetry] +name = "mirascope-cli" +version = "0.1.0" +description = "The Mirascope Command Line Interface" +license = "MIT" +authors = [ + "William Bakst ", + "Brendan Kao ", +] +readme = "docs/README.md" +packages = [{ include = "mirascope_cli" }] +repository = "https://github.com/Mirascope/mirascope-cli" + +[tool.poetry.scripts] +mirascope = 'mirascope_cli.main:app' + +[tool.poetry.dependencies] +python = ">=3.9,<3.13" +pydantic = "^2.0.2" +typer = { version = ">=0.9.0,<1.0.0", extras = ["all"] } +Jinja2 = "^3.1.3" + +[tool.poetry.group.dev.dependencies] +mypy = "^1.6.1" +pytest = "^7.4.0" +ruff = "^0.1.5" +pytest-asyncio = "^0.23.3" +pytest-cov = "^4.1.0" + +[tool.poetry.group.docs.dependencies] +mike = "^2.0.0" +mkdocs = "^1.4.3" +mkdocs-material = "^9.1.18" +mkdocstrings = "^0.22.0" +mkdocstrings-python = "^1.1.2" + +[tool.pytest.ini_options] +filterwarnings = ["ignore::DeprecationWarning"] + +[tool.ruff] +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", +] +line-length = 88 +target-version = "py38" + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["F401"] + +[tool.ruff.lint] +select = ["E4", "E7", "E9", "F"] +ignore = [] +fixable = ["ALL"] +unfixable = [] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.format] +quote-style = "double" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "auto" + +[tool.mypy] +exclude = ["venv", "virtualenvs"] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/commands/__init__.py b/tests/commands/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/commands/golden/base_prompt/0001_base_prompt.py b/tests/commands/golden/base_prompt/0001_base_prompt.py new file mode 100644 index 0000000..1d00320 --- /dev/null +++ b/tests/commands/golden/base_prompt/0001_base_prompt.py @@ -0,0 +1,11 @@ +"""A prompt for recommending movies of a particular genre.""" + +from mirascope import BasePrompt + +prev_revision_id = None +revision_id = "0001" + + +class MovieRecommendationPrompt(BasePrompt): + prompt_template = "Please recommend a list of movies in the {genre} category." + genre: str diff --git a/tests/commands/golden/base_prompt/0002_base_prompt.py b/tests/commands/golden/base_prompt/0002_base_prompt.py new file mode 100644 index 0000000..ea09227 --- /dev/null +++ b/tests/commands/golden/base_prompt/0002_base_prompt.py @@ -0,0 +1,11 @@ +"""A prompt for recommending movies of a particular genre.""" + +from mirascope import BasePrompt + +prev_revision_id = "0001" +revision_id = "0002" + + +class MovieRecommendationPrompt(BasePrompt): + prompt_template = "Please recommend a list of movies in the {genre} category." + genre: str diff --git a/tests/commands/golden/base_prompt/base_prompt.py b/tests/commands/golden/base_prompt/base_prompt.py new file mode 100644 index 0000000..eb3f218 --- /dev/null +++ b/tests/commands/golden/base_prompt/base_prompt.py @@ -0,0 +1,8 @@ +"""A prompt for recommending movies of a particular genre.""" +from mirascope import BasePrompt + + +class MovieRecommendationPrompt(BasePrompt): + prompt_template = "Please recommend a list of movies in the {genre} category." + + genre: str diff --git a/tests/commands/golden/base_prompt/base_prompt_with_changes.py b/tests/commands/golden/base_prompt/base_prompt_with_changes.py new file mode 100644 index 0000000..40d9391 --- /dev/null +++ b/tests/commands/golden/base_prompt/base_prompt_with_changes.py @@ -0,0 +1,14 @@ +"""A prompt for recommending movies of a particular genre.""" +from mirascope import BasePrompt + + +class MovieRecommendationPrompt(BasePrompt): + prompt_template = """ + Please recommend a list of movies in the {genre} category. I want the list to only + have 3 movies in total. Please order the list by the quality of the movie, with the + highest quality movie first. I want the movie list to include the movie title as + well as any notable actors in the movie. Please also include a short description of + the movie. + """ + + genre: str diff --git a/tests/commands/golden/base_prompt_auto_tag/0001_base_prompt_auto_tag.py b/tests/commands/golden/base_prompt_auto_tag/0001_base_prompt_auto_tag.py new file mode 100644 index 0000000..aad4112 --- /dev/null +++ b/tests/commands/golden/base_prompt_auto_tag/0001_base_prompt_auto_tag.py @@ -0,0 +1,14 @@ +"""A prompt for recommending movies of a particular genre.""" + +from mirascope import BasePrompt, tags + +prev_revision_id = None +revision_id = "0001" + + +@tags(["version:0001"]) +class MovieRecommendationPrompt(BasePrompt): + """A prompt for recommending movies.""" + + prompt_template = "Please recommend a list of movies in the {genre} category." + genre: str diff --git a/tests/commands/golden/base_prompt_auto_tag/0002_base_prompt_auto_tag.py b/tests/commands/golden/base_prompt_auto_tag/0002_base_prompt_auto_tag.py new file mode 100644 index 0000000..ee910b8 --- /dev/null +++ b/tests/commands/golden/base_prompt_auto_tag/0002_base_prompt_auto_tag.py @@ -0,0 +1,14 @@ +"""A prompt for recommending movies of a particular genre.""" + +from mirascope import BasePrompt, tags + +prev_revision_id = "0001" +revision_id = "0002" + + +@tags(["version:0002"]) +class MovieRecommendationPrompt(BasePrompt): + """A prompt for recommending movies.""" + + prompt_template = "Please recommend a list of movies in the {genre} category." + genre: str diff --git a/tests/commands/golden/base_prompt_auto_tag/base_prompt_auto_tag.py b/tests/commands/golden/base_prompt_auto_tag/base_prompt_auto_tag.py new file mode 100644 index 0000000..715e7d0 --- /dev/null +++ b/tests/commands/golden/base_prompt_auto_tag/base_prompt_auto_tag.py @@ -0,0 +1,10 @@ +"""A prompt for recommending movies of a particular genre.""" +from mirascope import BasePrompt + + +class MovieRecommendationPrompt(BasePrompt): + """A prompt for recommending movies.""" + + prompt_template = "Please recommend a list of movies in the {genre} category." + + genre: str diff --git a/tests/commands/golden/base_prompt_with_decorator/0001_base_prompt_with_decorator.py b/tests/commands/golden/base_prompt_with_decorator/0001_base_prompt_with_decorator.py new file mode 100644 index 0000000..5165971 --- /dev/null +++ b/tests/commands/golden/base_prompt_with_decorator/0001_base_prompt_with_decorator.py @@ -0,0 +1,25 @@ +"""A prompt for recommending movies of a particular genre.""" + +from mirascope import BasePrompt, tags + +prev_revision_id = None +revision_id = "0001" + + +@tags(["movie_project", "version:0001"]) +class MovieRecommendationPrompt(BasePrompt): + prompt_template = """ + SYSTEM: + You are the world's most knowledgeable movie buff. You know everything there is to + know about movies. You have likely seen every movie ever made. Your incredible + talent is your ability to recommend movies to people using only the genre of the + movie. The reason people love your recommendations so much is that they always + include succinct and clear descriptions of the movie. You also make sure to pique + their interest by mentioning any famous actors in the movie that might be of + interest. + + USER: + Please recommend 3 movies in the {genre} cetegory. + """ + + genre: str diff --git a/tests/commands/golden/base_prompt_with_decorator/0002_base_prompt_with_decorator.py b/tests/commands/golden/base_prompt_with_decorator/0002_base_prompt_with_decorator.py new file mode 100644 index 0000000..3869764 --- /dev/null +++ b/tests/commands/golden/base_prompt_with_decorator/0002_base_prompt_with_decorator.py @@ -0,0 +1,25 @@ +"""A prompt for recommending movies of a particular genre.""" + +from mirascope import BasePrompt, tags + +prev_revision_id = "0001" +revision_id = "0002" + + +@tags(["movie_project", "version:0001"]) +class MovieRecommendationPrompt(BasePrompt): + prompt_template = """ + SYSTEM: + You are the world's most knowledgeable movie buff. You know everything there is to + know about movies. You have likely seen every movie ever made. Your incredible + talent is your ability to recommend movies to people using only the genre of the + movie. The reason people love your recommendations so much is that they always + include succinct and clear descriptions of the movie. You also make sure to pique + their interest by mentioning any famous actors in the movie that might be of + interest. + + USER: + Please recommend 3 movies in the {genre} cetegory. + """ + + genre: str diff --git a/tests/commands/golden/base_prompt_with_decorator/base_prompt_with_decorator.py b/tests/commands/golden/base_prompt_with_decorator/base_prompt_with_decorator.py new file mode 100644 index 0000000..164c7b7 --- /dev/null +++ b/tests/commands/golden/base_prompt_with_decorator/base_prompt_with_decorator.py @@ -0,0 +1,21 @@ +"""A prompt for recommending movies of a particular genre.""" +from mirascope import BasePrompt, tags + + +@tags(["movie_project", "version:0001"]) +class MovieRecommendationPrompt(BasePrompt): + prompt_template = """ + SYSTEM: + You are the world's most knowledgeable movie buff. You know everything there is to + know about movies. You have likely seen every movie ever made. Your incredible + talent is your ability to recommend movies to people using only the genre of the + movie. The reason people love your recommendations so much is that they always + include succinct and clear descriptions of the movie. You also make sure to pique + their interest by mentioning any famous actors in the movie that might be of + interest. + + USER: + Please recommend 3 movies in the {genre} cetegory. + """ + + genre: str diff --git a/tests/commands/golden/call_with_variables/0001_call_with_variables.py b/tests/commands/golden/call_with_variables/0001_call_with_variables.py new file mode 100644 index 0000000..26fafe8 --- /dev/null +++ b/tests/commands/golden/call_with_variables/0001_call_with_variables.py @@ -0,0 +1,31 @@ +"""A call for recommending movies of a particular genre.""" + +from mirascope import tags +from mirascope.openai import OpenAICall, OpenAICallParams + +prev_revision_id = None +revision_id = "0001" +number = 1 +chat = OpenAICall() +a_list = [1, 2, 3] + + +@tags(["movie_project", "version:0001"]) +class MovieRecommender(OpenAICall): + """An LLM call for recommending movies, using OpenAI.""" + + prompt_template = """ + SYSTEM: + You are the world's most knowledgeable movie buff. You know everything there is to + know about movies. You have likely seen every movie ever made. Your incredible + talent is your ability to recommend movies to people using only the genre of the + movie. The reason people love your recommendations so much is that they always + include succinct and clear descriptions of the movie. You also make sure to pique + their interest by mentioning any famous actors in the movie that might be of + interest. + + USER: + Please recommend 3 movies in the {genre} cetegory. + """ + genre: str + call_params = OpenAICallParams(model="gpt-3.5-turbo") diff --git a/tests/commands/golden/call_with_variables/0002_call_with_variables.py b/tests/commands/golden/call_with_variables/0002_call_with_variables.py new file mode 100644 index 0000000..fd414c2 --- /dev/null +++ b/tests/commands/golden/call_with_variables/0002_call_with_variables.py @@ -0,0 +1,31 @@ +"""A call for recommending movies of a particular genre.""" + +from mirascope import tags +from mirascope.openai import OpenAICall, OpenAICallParams + +prev_revision_id = "0001" +revision_id = "0002" +number = 1 +chat = OpenAICall() +a_list = [1, 2, 3] + + +@tags(["movie_project", "version:0001"]) +class MovieRecommender(OpenAICall): + """An LLM call for recommending movies, using OpenAI.""" + + prompt_template = """ + SYSTEM: + You are the world's most knowledgeable movie buff. You know everything there is to + know about movies. You have likely seen every movie ever made. Your incredible + talent is your ability to recommend movies to people using only the genre of the + movie. The reason people love your recommendations so much is that they always + include succinct and clear descriptions of the movie. You also make sure to pique + their interest by mentioning any famous actors in the movie that might be of + interest. + + USER: + Please recommend 3 movies in the {genre} cetegory. + """ + genre: str + call_params = OpenAICallParams(model="gpt-3.5-turbo") diff --git a/tests/commands/golden/call_with_variables/call_with_variables.py b/tests/commands/golden/call_with_variables/call_with_variables.py new file mode 100644 index 0000000..9e16d69 --- /dev/null +++ b/tests/commands/golden/call_with_variables/call_with_variables.py @@ -0,0 +1,30 @@ +"""A call for recommending movies of a particular genre.""" +from mirascope import tags +from mirascope.openai import OpenAICall, OpenAICallParams + +number = 1 +chat = OpenAICall() +a_list = [1, 2, 3] + + +@tags(["movie_project", "version:0001"]) +class MovieRecommender(OpenAICall): + """An LLM call for recommending movies, using OpenAI.""" + + prompt_template = """ + SYSTEM: + You are the world's most knowledgeable movie buff. You know everything there is to + know about movies. You have likely seen every movie ever made. Your incredible + talent is your ability to recommend movies to people using only the genre of the + movie. The reason people love your recommendations so much is that they always + include succinct and clear descriptions of the movie. You also make sure to pique + their interest by mentioning any famous actors in the movie that might be of + interest.\n + + USER: + Please recommend 3 movies in the {genre} cetegory. + """ + + genre: str + + call_params = OpenAICallParams(model="gpt-3.5-turbo") diff --git a/tests/commands/golden/extractor_with_pydantic/0001_extractor_with_pydantic.py b/tests/commands/golden/extractor_with_pydantic/0001_extractor_with_pydantic.py new file mode 100644 index 0000000..163a83a --- /dev/null +++ b/tests/commands/golden/extractor_with_pydantic/0001_extractor_with_pydantic.py @@ -0,0 +1,52 @@ +"""A prompt for asking a question about a paragraph of text. + +{ + "exact": 85.34482758620689, + "f1": 91.55579573683022, + "total": 116, + "HasAns_exact": 85.34482758620689, + "HasAns_f1": 91.55579573683022, + "HasAns_total": 116 +} +""" +from typing import Type + +from pydantic import BaseModel, Field + +from mirascope.openai import OpenAIExtractor + +prev_revision_id = None +revision_id = "0001" + + +class Answer(BaseModel): + """The answer to a question about a paragraph of text.""" + + answer: str = Field( + ..., + description=( + "The extracted answer to the question. This answer is as concise as " + "possible, most often just a single word. It is also an exact text match " + "with text in the provided context." + ), + ) + + +class Answerer(OpenAIExtractor[Answer]): + extract_schema: Type[Answer] = Answer + prompt_template = """ + SYSTEM: + You will be asked a question after you read a paragraph. Your task is to + answer the question based on the information in the paragraph. Your answer + should be an exact text match to text from the paragraph. Your answer should + also be one or two words at most is possible. + + USER: + {paragraph} + + USER: + {question} + """ + + paragraph: str + question: str diff --git a/tests/commands/golden/extractor_with_pydantic/extractor_with_pydantic.py b/tests/commands/golden/extractor_with_pydantic/extractor_with_pydantic.py new file mode 100644 index 0000000..25ba4ae --- /dev/null +++ b/tests/commands/golden/extractor_with_pydantic/extractor_with_pydantic.py @@ -0,0 +1,52 @@ +"""A prompt for asking a question about a paragraph of text. + +{ + "exact": 85.34482758620689, + "f1": 91.55579573683022, + "total": 116, + "HasAns_exact": 85.34482758620689, + "HasAns_f1": 91.55579573683022, + "HasAns_total": 116 +} +""" +from typing import Type + +from pydantic import BaseModel, Field + +from mirascope.openai import OpenAIExtractor + + +class Answer(BaseModel): + """The answer to a question about a paragraph of text.""" + + answer: str = Field( + ..., + description=( + "The extracted answer to the question. This answer is as concise as " + "possible, most often just a single word. It is also an exact text match " + "with text in the provided context." + ), + ) + + +class Answerer(OpenAIExtractor[Answer]): + extract_schema: Type[Answer] = Answer + prompt_template = """ + SYSTEM: + You will be asked a question after you read a paragraph. Your task is to + answer the question based on the information in the paragraph. Your answer + should be an exact text match to text from the paragraph. Your answer should + also be one or two words at most is possible. + + USER: + {paragraph} + + USER: + {question} + """ + + paragraph: str + question: str + + +answer = Answerer(paragraph="", question="").extract() diff --git a/tests/commands/golden/version.txt b/tests/commands/golden/version.txt new file mode 100644 index 0000000..f2dc03d --- /dev/null +++ b/tests/commands/golden/version.txt @@ -0,0 +1,2 @@ +CURRENT_REVISION=0001 +LATEST_REVISION=0001 diff --git a/tests/commands/test_add.py b/tests/commands/test_add.py new file mode 100644 index 0000000..ab42547 --- /dev/null +++ b/tests/commands/test_add.py @@ -0,0 +1,149 @@ +"""Test for mirascope cli add command functions.""" +import os +import shutil +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from typer.testing import CliRunner + +from mirascope_cli import app +from mirascope_cli.schemas import MirascopeSettings, VersionTextFile + +runner = CliRunner() + + +def _initialize_tmp_mirascope(tmp_path: Path, golden_prompt: str): + """Initializes a temporary mirascope directory with the specified prompt.""" + golden_prompt_directory = golden_prompt + if not golden_prompt.endswith(".py"): + golden_prompt = f"{golden_prompt}.py" + source_file = ( + Path(__file__).parent / "golden" / golden_prompt_directory / golden_prompt + ) + destination_dir_prompts = tmp_path / "prompts" + destination_dir_prompts.mkdir() + shutil.copy(source_file, destination_dir_prompts / golden_prompt) + destination_dir_mirascope_dir = tmp_path / ".mirascope" + destination_dir_mirascope_dir.mkdir() + prompt_template_path = ( + Path(__file__).parent.parent.parent / "mirascope_cli/generic/prompt_template.j2" + ) + shutil.copy( + prompt_template_path, destination_dir_mirascope_dir / "prompt_template.j2" + ) + + +@pytest.mark.parametrize( + "golden_prompt,auto_tag", + [ + ("base_prompt", False), + ("base_prompt", True), + ], +) +@pytest.mark.parametrize( + "version_text_file", + [ + VersionTextFile(current_revision=None, latest_revision=None), + VersionTextFile(current_revision="0001", latest_revision="0001"), + ], +) +@patch("mirascope_cli.utils.get_user_mirascope_settings") +@patch("mirascope_cli.commands.add.get_user_mirascope_settings") +@patch("mirascope_cli.commands.add.get_prompt_versions") +def test_add( + mock_get_prompt_versions: MagicMock, + mock_get_mirascope_settings_add: MagicMock, + mock_get_mirascope_settings: MagicMock, + golden_prompt: str, + auto_tag: bool, + version_text_file: VersionTextFile, + tmp_path: Path, +): + """Tests that `add` adds a prompt to the specified version directory.""" + mirascope_settings = MirascopeSettings( + mirascope_location=".mirascope", + version_file_name="version.txt", + prompts_location="prompts", + versions_location=".mirascope/versions", + format_command="ruff check --select I --fix; ruff format", + auto_tag=auto_tag, + ) + mock_get_mirascope_settings_add.return_value = mirascope_settings + mock_get_mirascope_settings.return_value = mirascope_settings + mock_get_prompt_versions.return_value = version_text_file + current_revision = version_text_file.current_revision + next_revision = ( + "0001" if current_revision is None else f"{int(current_revision)+1:04d}" + ) + with runner.isolated_filesystem(temp_dir=tmp_path) as td: + if mirascope_settings.auto_tag: + golden_prompt = f"{golden_prompt}_auto_tag" + _initialize_tmp_mirascope(Path(td), golden_prompt) + result = runner.invoke( + app, + ["add", golden_prompt], + ) + assert ( + result.output.strip() + == f"Adding {mirascope_settings.versions_location}/{golden_prompt}/{next_revision}_{golden_prompt}.py" + ) + golden_prompt_version_file = f"{next_revision}_{golden_prompt}" + assert os.path.exists( + Path(td) + / mirascope_settings.versions_location + / golden_prompt + / f"{next_revision}_{golden_prompt}.py" + ) + source_file = ( + Path(__file__).parent + / "golden" + / golden_prompt + / f"{golden_prompt_version_file}.py" + ) + with open( + Path(td) + / mirascope_settings.versions_location + / golden_prompt + / f"{next_revision}_{golden_prompt}.py", + "r", + encoding="utf-8", + ) as file: + assert file.read() == source_file.read_text() + + +@patch("mirascope_cli.commands.add.get_user_mirascope_settings") +def test_add_unknown_file(mock_get_mirascope_settings_add: MagicMock): + """Tests that `add` fails when the prompt file does not exist.""" + mock_get_mirascope_settings_add.return_value = MirascopeSettings( + mirascope_location=".mirascope", + auto_tag=True, + version_file_name="version.txt", + prompts_location="prompts", + versions_location=".mirascope/versions", + ) + with pytest.raises(FileNotFoundError): + runner.invoke(app, ["add", "unknown_prompt"], catch_exceptions=False) + + +@patch("mirascope_cli.commands.add.check_status") +@patch("mirascope_cli.commands.add.get_user_mirascope_settings") +def test_add_no_changes( + mock_get_mirascope_settings_add: MagicMock, + mock_check_status: MagicMock, + tmp_path: Path, +): + """Tests that `add` does nothing when prompt did not change.""" + mock_check_status.return_value = None + mock_get_mirascope_settings_add.return_value = MirascopeSettings( + mirascope_location=".mirascope", + auto_tag=True, + version_file_name="version.txt", + prompts_location="prompts", + versions_location=".mirascope/versions", + ) + prompt = "base_prompt" + with runner.isolated_filesystem(temp_dir=tmp_path) as td: + _initialize_tmp_mirascope(Path(td), prompt) + result = runner.invoke(app, ["add", prompt]) + assert result.output.strip() == "No changes detected." diff --git a/tests/commands/test_init.py b/tests/commands/test_init.py new file mode 100644 index 0000000..20eb420 --- /dev/null +++ b/tests/commands/test_init.py @@ -0,0 +1,59 @@ +"""Test for mirascope cli init command functions.""" +import os +from pathlib import Path + +import pytest +from typer.testing import CliRunner + +from mirascope_cli import app + +runner = CliRunner() + + +@pytest.mark.parametrize( + "mirascope_location, prompts_location", [(".foo", "bar"), (None, None)] +) +def test_init_command( + mirascope_location: str, + prompts_location: str, + tmp_path: Path, +): + """Tests that `init` command creates the necessary directories and files.""" + with runner.isolated_filesystem(temp_dir=tmp_path) as td: + mirascope_location_args = ["--mirascope-location", mirascope_location] + prompts_location_args = ["--prompts-location", prompts_location] + + if mirascope_location is None: + mirascope_location_args = [] + mirascope_location = ".mirascope" + + if prompts_location is None: + prompts_location_args = [] + prompts_location = "prompts" + + result = runner.invoke( + app, + [ + "init", + ] + + mirascope_location_args + + prompts_location_args, + ) + results = result.output.split("\n") + assert os.path.exists(f"{td}/{mirascope_location}") + assert os.path.exists(f"{td}/{mirascope_location}/versions") + assert os.path.exists(f"{td}/{prompts_location}") + assert os.path.exists(f"{td}/{prompts_location}/__init__.py") + assert os.path.exists(f"{td}/mirascope.ini") + assert os.path.exists(f"{td}/{mirascope_location}/prompt_template.j2") + + assert results[0].strip() == f"Creating {td}/{mirascope_location}/versions" + assert results[1].strip() == f"Creating {td}/{prompts_location}" + assert results[2].strip() == f"Creating {td}/{prompts_location}/__init__.py" + assert results[3].strip() == f"Creating {td}/mirascope.ini" + assert ( + results[4].strip() + == f"Creating {td}/{mirascope_location}/prompt_template.j2" + ) + assert results[5].strip() == "Initialization complete." + assert result.exit_code == 0 diff --git a/tests/commands/test_remove.py b/tests/commands/test_remove.py new file mode 100644 index 0000000..f543e3c --- /dev/null +++ b/tests/commands/test_remove.py @@ -0,0 +1,132 @@ +"""Test for mirascope cli remove commands functions.""" +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from typer.testing import CliRunner + +from mirascope_cli import app +from mirascope_cli.schemas import MirascopeSettings, VersionTextFile + +runner = CliRunner() + + +@patch("mirascope_cli.commands.remove.get_user_mirascope_settings") +@patch("mirascope_cli.commands.remove.get_prompt_versions") +@patch("mirascope_cli.commands.remove.find_prompt_path") +@patch("mirascope_cli.commands.remove.find_prompt_paths") +@patch("os.remove") +def test_remove( + mock_remove: MagicMock, + mock_prompt_paths: MagicMock, + mock_prompt_path: MagicMock, + mock_prompt_versions: MagicMock, + mock_settings: MagicMock, + fixture_mirascope_user_settings: MirascopeSettings, + fixture_prompt_versions: VersionTextFile, + tmp_path: Path, +): + """Tests that `remove` removes a prompt and detaches any prev_revision_id.""" + version = "0001" + version2 = "0002" + version_path = tmp_path / f"{version}_test.py" + version2_path = tmp_path / f"{version2}_test.py" + mock_settings.return_value = fixture_mirascope_user_settings + mock_prompt_versions.return_value = fixture_prompt_versions + mock_prompt_paths.return_value = [version_path, version2_path] + mock_prompt_path.return_value = mock_prompt_paths.return_value[0] + prompt_file_name = "dan" + version_path.write_text("prev_revision_id = None\n") + version2_path.write_text(f"prev_revision_id = '{version}'\n") + + result = runner.invoke(app, ["remove", prompt_file_name, version]) + + mock_remove.assert_called_once_with(mock_prompt_path.return_value) + assert ( + result.output.strip() + == f"Detached {version2_path}\nPrompt {prompt_file_name} {version} " + "successfully removed" + ) + assert result.exit_code == 0 + + +@patch("mirascope_cli.commands.remove.get_user_mirascope_settings") +@patch("mirascope_cli.commands.remove.get_prompt_versions") +@patch("mirascope_cli.commands.remove.find_prompt_path") +@patch("mirascope_cli.commands.remove.find_prompt_paths") +@patch("os.remove") +def test_remove_no_prompt_paths( + mock_remove: MagicMock, + mock_prompt_paths: MagicMock, + mock_prompt_path: MagicMock, + mock_prompt_versions: MagicMock, + mock_settings: MagicMock, + fixture_mirascope_user_settings: MirascopeSettings, + fixture_prompt_versions: VersionTextFile, + tmp_path: Path, +): + """Tests that `remove` removes a prompt""" + version = "0001" + version_path = tmp_path / f"{version}_test.py" + mock_settings.return_value = fixture_mirascope_user_settings + mock_prompt_versions.return_value = fixture_prompt_versions + mock_prompt_paths.return_value = None + mock_prompt_path.return_value = version_path + prompt_file_name = "dan" + + result = runner.invoke(app, ["remove", prompt_file_name, version]) + + mock_remove.assert_called_once_with(mock_prompt_path.return_value) + assert ( + result.output.strip() == f"Prompt {prompt_file_name} {version} " + "successfully removed" + ) + assert result.exit_code == 0 + + +@patch("mirascope_cli.commands.remove.get_user_mirascope_settings") +@patch("mirascope_cli.commands.remove.get_prompt_versions") +@patch("os.remove") +def test_remove_current_revision_collision( + mock_remove: MagicMock, + mock_prompt_versions: MagicMock, + mock_settings: MagicMock, + fixture_mirascope_user_settings: MirascopeSettings, + fixture_prompt_versions: VersionTextFile, +): + """Tests that `remove` does not remove a prompt if prompt is active.""" + mock_settings.return_value = fixture_mirascope_user_settings + mock_prompt_versions.return_value = fixture_prompt_versions + version: str = ( + fixture_prompt_versions.current_revision + if fixture_prompt_versions.current_revision + else "" + ) + prompt_file_name = "test" + result = runner.invoke(app, ["remove", prompt_file_name, version]) + + assert mock_remove.call_count == 0 + assert ( + result.output.strip() + == f"Prompt {prompt_file_name} {version} is currently being used. " + "Please switch to another version first." + ) + + +@patch("mirascope_cli.commands.remove.get_user_mirascope_settings") +@patch("mirascope_cli.commands.remove.get_prompt_versions") +@patch("mirascope_cli.commands.remove.find_prompt_path") +def test_remove_file_not_found( + mock_prompt_path: MagicMock, + mock_prompt_versions: MagicMock, + mock_settings: MagicMock, + fixture_mirascope_user_settings: MirascopeSettings, + fixture_prompt_versions: VersionTextFile, +): + """Tests that `remove` raises a FileNotFoundError if prompt file is not found.""" + mock_prompt_path.return_value = None + mock_settings.return_value = fixture_mirascope_user_settings + mock_prompt_versions.return_value = fixture_prompt_versions + + with pytest.raises(FileNotFoundError): + runner.invoke(app, ["remove", "test", "0001"], catch_exceptions=False) diff --git a/tests/commands/test_status.py b/tests/commands/test_status.py new file mode 100644 index 0000000..bd855fc --- /dev/null +++ b/tests/commands/test_status.py @@ -0,0 +1,94 @@ +"""Test for mirascope cli status command functions.""" +import shutil +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from typer.testing import CliRunner + +from mirascope_cli import app +from mirascope_cli.schemas import MirascopeSettings + +runner = CliRunner() + + +def _initialize_tmp_mirascope(tmp_path: Path, golden_prompt: str, golden_version: str): + """Initializes a temporary mirascope directory with prompt `base_prompt`.""" + golden_prompt_directory = "base_prompt" + if not golden_prompt.endswith(".py"): + golden_prompt = f"{golden_prompt}.py" + if not golden_version.endswith(".py"): + golden_version = f"{golden_version}.py" + golden_prompt_source_file = ( + Path(__file__).parent / "golden" / golden_prompt_directory / golden_prompt + ) + destination_dir_prompts = tmp_path / "prompts" + destination_dir_prompts.mkdir() + shutil.copy(golden_prompt_source_file, destination_dir_prompts / "base_prompt.py") + destination_dir_mirascope_dir = tmp_path / ".mirascope" + destination_dir_mirascope_dir.mkdir() + golden_version_source_file = ( + Path(__file__).parent / "golden" / golden_prompt_directory / golden_version + ) + version_text_file = Path(__file__).parent / "golden" / "version.txt" + golden_prompts_dir = ( + destination_dir_mirascope_dir / "versions" / golden_prompt_directory + ) + golden_prompts_dir.mkdir(parents=True) + shutil.copy(golden_version_source_file, golden_prompts_dir / golden_version) + shutil.copy(version_text_file, golden_prompts_dir / "version.txt") + prompt_template_path = ( + Path(__file__).parent.parent.parent / "mirascope_cli/generic/prompt_template.j2" + ) + shutil.copy( + prompt_template_path, destination_dir_mirascope_dir / "prompt_template.j2" + ) + + +@pytest.mark.parametrize("golden_prompt", ["base_prompt", "base_prompt_with_changes"]) +@patch("mirascope_cli.commands.status.get_user_mirascope_settings") +def test_status_with_arg( + mock_get_mirascope_settings_status: MagicMock, + golden_prompt: str, + fixture_mirascope_user_settings: MirascopeSettings, + tmp_path: Path, +): + """Tests that `status` with arguments returns the correct status.""" + mock_get_mirascope_settings_status.return_value = fixture_mirascope_user_settings + prompts_location = fixture_mirascope_user_settings.prompts_location + prompt = "base_prompt" + with runner.isolated_filesystem(temp_dir=tmp_path) as td: + _initialize_tmp_mirascope(Path(td), golden_prompt, f"0001_{prompt}") + result = runner.invoke(app, ["status", prompt]) + if golden_prompt == f"{prompt}_with_changes": + assert ( + result.output.strip() + == f"Prompt {prompts_location}/{prompt}.py has changed." + ) + else: + assert result.output.strip() == "No changes detected." + assert result.exit_code == 0 + + +@pytest.mark.parametrize("golden_prompt", ["base_prompt", "base_prompt_with_changes"]) +@patch("mirascope_cli.commands.status.get_user_mirascope_settings") +def test_status_no_args( + mock_get_mirascope_settings_status: MagicMock, + golden_prompt: str, + fixture_mirascope_user_settings: MirascopeSettings, + tmp_path: Path, +): + """Tests that `status` with no arguments returns the correct status.""" + mock_get_mirascope_settings_status.return_value = fixture_mirascope_user_settings + prompts_location = fixture_mirascope_user_settings.prompts_location + with runner.isolated_filesystem(temp_dir=tmp_path) as td: + _initialize_tmp_mirascope(Path(td), golden_prompt, "0001_base_prompt") + result = runner.invoke(app, ["status"]) + if golden_prompt == "base_prompt_with_changes": + results = result.output.splitlines() + assert results[0].strip() == "The following prompts have changed:" + assert results[1].strip() == f"{prompts_location}/base_prompt.py" + + else: + assert result.output.strip() == "No changes detected." + assert result.exit_code == 0 diff --git a/tests/commands/test_use.py b/tests/commands/test_use.py new file mode 100644 index 0000000..5f495c7 --- /dev/null +++ b/tests/commands/test_use.py @@ -0,0 +1,123 @@ +"""Test for mirascope cli use command functions.""" +import shutil +from pathlib import Path +from unittest.mock import MagicMock, patch + +import pytest +from typer.testing import CliRunner + +from mirascope_cli import app +from mirascope_cli.schemas import MirascopeSettings + +runner = CliRunner() + + +def _initialize_tmp_mirascope( + tmp_path: Path, golden_prompt: str, golden_versions: list[str] +): + """Initializes a temporary mirascope directory with prompt `base_prompt`.""" + golden_prompt_directory = "base_prompt" + if not golden_prompt.endswith(".py"): + golden_prompt = f"{golden_prompt}.py" + golden_versions = [ + f"{golden_version}.py" + for golden_version in golden_versions + if not golden_version.endswith(".py") + ] + + golden_prompt_source_file = ( + Path(__file__).parent / "golden" / golden_prompt_directory / golden_prompt + ) + destination_dir_prompts = tmp_path / "prompts" + destination_dir_prompts.mkdir() + shutil.copy(golden_prompt_source_file, destination_dir_prompts / "base_prompt.py") + destination_dir_mirascope_dir = tmp_path / ".mirascope" + destination_dir_mirascope_dir.mkdir() + golden_prompts_dir = ( + destination_dir_mirascope_dir / "versions" / golden_prompt_directory + ) + golden_prompts_dir.mkdir(parents=True) + version_text_file = Path(__file__).parent / "golden" / "version.txt" + shutil.copy(version_text_file, golden_prompts_dir / "version.txt") + for golden_version in golden_versions: + golden_version_source_file = ( + Path(__file__).parent / "golden" / golden_prompt_directory / golden_version + ) + shutil.copy(golden_version_source_file, golden_prompts_dir / golden_version) + prompt_template_path = ( + Path(__file__).parent.parent.parent / "mirascope_cli/generic/prompt_template.j2" + ) + shutil.copy( + prompt_template_path, destination_dir_mirascope_dir / "prompt_template.j2" + ) + + +@pytest.mark.parametrize("golden_prompt", ["base_prompt"]) +@patch("mirascope_cli.utils.get_user_mirascope_settings") +@patch("mirascope_cli.commands.use.get_user_mirascope_settings") +def test_use_command( + mock_get_mirascope_settings_use: MagicMock, + mock_get_mirascope_settings: MagicMock, + golden_prompt: str, + fixture_mirascope_user_settings: MirascopeSettings, + tmp_path: Path, +): + """Tests that `use` command updates the prompt file""" + mock_get_mirascope_settings_use.return_value = fixture_mirascope_user_settings + mock_get_mirascope_settings.return_value = fixture_mirascope_user_settings + prompt = "base_prompt" + with runner.isolated_filesystem(temp_dir=tmp_path) as td: + _initialize_tmp_mirascope( + Path(td), golden_prompt, [f"0001_{prompt}", f"0002_{prompt}"] + ) + result = runner.invoke(app, ["use", prompt, "0002"]) + assert result.exit_code == 0 + + +@patch("mirascope_cli.utils.get_user_mirascope_settings") +@patch("mirascope_cli.commands.use.get_user_mirascope_settings") +def test_use_command_file_changed( + mock_get_mirascope_settings_use: MagicMock, + mock_get_mirascope_settings: MagicMock, + fixture_mirascope_user_settings: MirascopeSettings, + tmp_path: Path, +): + """Tests that `use` does not update the prompt file if it has changes.""" + mock_get_mirascope_settings_use.return_value = fixture_mirascope_user_settings + mock_get_mirascope_settings.return_value = fixture_mirascope_user_settings + prompt = "base_prompt" + with runner.isolated_filesystem(temp_dir=tmp_path) as td: + _initialize_tmp_mirascope( + Path(td), "base_prompt_with_changes", [f"0001_{prompt}"] + ) + result = runner.invoke(app, ["use", prompt, "0002"]) + results = result.output.splitlines() + assert ( + results[0].strip() + == "Changes detected, please add or remove changes first." + ) + assert results[1].strip() == f"mirascope add {prompt}" + + assert result.exit_code == 0 + + +@patch("mirascope_cli.utils.get_user_mirascope_settings") +@patch("mirascope_cli.commands.use.get_user_mirascope_settings") +def test_use_no_version_file( + mock_get_mirascope_settings_use: MagicMock, + mock_get_mirascope_settings: MagicMock, + fixture_mirascope_user_settings: MirascopeSettings, + tmp_path: Path, +): + """Tests that `use` raises a FileNotFoundError if the version file is not found. + + This test first checks if status has changes, then raises a FileNotFoundError when + trying to use a version file that does not exist. + """ + mock_get_mirascope_settings_use.return_value = fixture_mirascope_user_settings + mock_get_mirascope_settings.return_value = fixture_mirascope_user_settings + prompt = "base_prompt" + with runner.isolated_filesystem(temp_dir=tmp_path) as td: + _initialize_tmp_mirascope(Path(td), prompt, [f"0001_{prompt}"]) + with pytest.raises(FileNotFoundError): + runner.invoke(app, ["use", prompt, "0002"], catch_exceptions=False) diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..b8788ae --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,23 @@ +"""Fixtures for CLI tests.""" +import pytest + +from mirascope_cli.schemas import MirascopeSettings, VersionTextFile + + +@pytest.fixture() +def fixture_mirascope_user_settings() -> MirascopeSettings: + """Returns a `MirascopeSettings` instance.""" + return MirascopeSettings( + format_command="ruff check --select I --fix; ruff format", + mirascope_location=".mirascope", + prompts_location="prompts", + version_file_name="version.txt", + versions_location=".mirascope/versions", + auto_tag=True, + ) + + +@pytest.fixture +def fixture_prompt_versions() -> VersionTextFile: + """Returns a `VersionTextFile` instance.""" + return VersionTextFile(current_revision="0002", latest_revision="0002") diff --git a/tests/test_utils.py b/tests/test_utils.py new file mode 100644 index 0000000..590fa38 --- /dev/null +++ b/tests/test_utils.py @@ -0,0 +1,421 @@ +"""Test for mirascope cli utility functions.""" +import os +from configparser import MissingSectionHeaderError +from pathlib import Path +from textwrap import dedent +from typing import Literal, Optional +from unittest.mock import MagicMock, patch + +import pytest +from jinja2 import Environment +from pydantic import ValidationError + +from mirascope_cli.constants import CURRENT_REVISION_KEY, LATEST_REVISION_KEY +from mirascope_cli.enums import MirascopeCommand +from mirascope_cli.schemas import ( + ClassInfo, + FunctionInfo, + MirascopeCliVariables, + MirascopeSettings, +) +from mirascope_cli.utils import ( + PromptAnalyzer, + check_prompt_changed, + find_file_names, + find_prompt_path, + get_prompt_analyzer, + get_prompt_versions, + get_user_mirascope_settings, + parse_prompt_file_name, + prompts_directory_files, + write_prompt_to_template, +) + + +def _get_mirascope_ini() -> str: + """Get the contents of the mirascope.ini.j2 file""" + mirascope_ini_path = Path("mirascope_cli/generic/mirascope.ini.j2") + return mirascope_ini_path.read_text(encoding="utf-8") + + +def _get_prompt_template() -> str: + """Get the contents of the prompt_template.j2 file""" + prompt_template_path = Path("mirascope_cli/generic/prompt_template.j2") + return prompt_template_path.read_text(encoding="utf-8") + + +def test_valid_mirascope_settings(tmp_path: Path): + """Tests that the ini file properly maps to pydantic model""" + mirascope_ini = _get_mirascope_ini() + ini_path = tmp_path / "settings.ini" + ini_path.write_text(mirascope_ini) + settings = get_user_mirascope_settings(str(ini_path)) + assert isinstance(settings, MirascopeSettings) + + +def test_invalid_mirascope_settings(tmp_path: Path): + """Tests that an invalid ini file raises pydantic ValidationError""" + mirascope_ini = _get_mirascope_ini() + additional_settings = "invalid_setting = 1" + invalid_mirascope_ini = mirascope_ini + "\n" + additional_settings + ini_path = tmp_path / "settings.ini" + + ini_path.write_text(invalid_mirascope_ini) + ini_path_no_section = tmp_path / "bad_settings.ini" + ini_path_no_section.write_text('invalid_settings = "no [mirascope] section"s') + with pytest.raises(FileNotFoundError): + get_user_mirascope_settings("invalid_path") + with pytest.raises(ValidationError): + get_user_mirascope_settings(str(ini_path)) + with pytest.raises(MissingSectionHeaderError): + get_user_mirascope_settings(str(ini_path_no_section)) + + +def _write_version_text_file(tmp_path: Path): + """Writes a version.txt file to the temporary directory""" + content = dedent( + f""" + {CURRENT_REVISION_KEY}=0002 + {LATEST_REVISION_KEY}=0003 + """ + ) + file_path = tmp_path / "version.txt" + file_path.write_text(content, encoding="utf-8") + return str(file_path) + + +def test_get_prompt_versions(tmp_path: Path): + """Tests that the version.txt file is properly read""" + versions = get_prompt_versions(_write_version_text_file(tmp_path)) + assert versions.current_revision == "0002" + assert versions.latest_revision == "0003" + + +def test_get_invalid_path_prompt_versions(): + """Tests that missing version.txt version is properly handled""" + versions = get_prompt_versions("invalid_path") + assert versions.current_revision is None + assert versions.latest_revision is None + + +def _write_python_prompt_file(tmp_path: Path, directory_name: str, content: str): + """Returns the contents of a python prompt file""" + file_name = f"{directory_name}.py" + file_path = tmp_path / file_name + file_path.write_text(content, encoding="utf-8") + return str(file_path) + + +def test_find_prompt_path(tmp_path: Path): + """Tests that the prompt path is properly found given only a prefix""" + version = "0001" + directory_name = f"{version}_my_prompt" + content = dedent( + """ + print('Hello World!) + """ + ) + prompt_path = _write_python_prompt_file(tmp_path, directory_name, content) + found_prompt_path = find_prompt_path(tmp_path, version) + assert prompt_path == found_prompt_path + + +@pytest.mark.parametrize( + "first_content, second_content", + [ + # imports + ( + dedent( + """ + import bar + """ + ), + dedent( + """ + import foo + """ + ), + ), + # from_imports + ( + dedent( + """ + from bar import foo + """ + ), + dedent( + """ + from foo import bar + """ + ), + ), + # variables + ( + dedent( + """ + foo = 'bar' + """ + ), + dedent( + """ + baz = 'qux' + """ + ), + ), + # classes + ( + dedent( + """ + class Bar(Foo): + foo = 'bar' + """ + ), + dedent( + """ + class Bar(Foo): + foo = 'baz' + """ + ), + ), + # comments + ( + dedent( + ''' + """This is a comment""" + ''' + ), + dedent( + ''' + """This is a different comment""" + ''' + ), + ), + ], +) +def test_check_prompt_changed_import( + tmp_path: Path, first_content: str, second_content: str +): + """Tests that the prompt is properly detected as changed given different content""" + directory_name = "my_prompt" + first_version_path = _write_python_prompt_file( + tmp_path, f"0001_{directory_name}", first_content + ) + second_version_path = _write_python_prompt_file( + tmp_path, f"0002_{directory_name}", second_content + ) + assert check_prompt_changed(first_version_path, second_version_path) + + +def test_check_prompt_changed_errors(tmp_path: Path): + directory_name = "my_prompt" + first_version_path = _write_python_prompt_file( + tmp_path, f"0001_{directory_name}", "" + ) + second_version_path = _write_python_prompt_file( + tmp_path, f"0002_{directory_name}", "" + ) + with pytest.raises(FileNotFoundError): + check_prompt_changed(first_version_path, "invalid_path2") + with pytest.raises(FileNotFoundError): + check_prompt_changed("invalid_path1", second_version_path) + with pytest.raises(FileNotFoundError): + check_prompt_changed(None, None) + + +def test_get_prompt_analyzer(): + """Tests that a prompt is properly created from the template""" + + sample_file_content = dedent( + ''' + """This is a comment""" + from fastapi import FastAPI + + app = FastAPI() + foo = "bar" + number = 1 + a_list = [1, 2, 3] + + @app.get("/") + async def root() -> dict[str, str]: + """This is root""" + return {"message": "Hello World"} + + def a_function(foo: str, bar: int) -> None: + pass + + ''' + ) + analyzer = get_prompt_analyzer(sample_file_content) + assert analyzer.comments == "This is a comment" + assert analyzer.from_imports == [("fastapi", "FastAPI", None)] + assert analyzer.variables == { + "foo": "'bar'", + "number": "1", + "a_list": "[1, 2, 3]", + "app": "FastAPI()", + } + assert analyzer.functions[0] == FunctionInfo( + name="root", + docstring="This is root", + args=[], + body="return {'message': 'Hello World'}", + decorators=["app.get('/')"], + returns="dict[str, str]", + is_async=True, + ) + assert analyzer.functions[1] == FunctionInfo( + name="a_function", + docstring=None, + args=["foo: str", "bar: int"], + body="pass", + decorators=[], + returns="None", + is_async=False, + ) + + +def test_prompt_analyzer_definition_changed(): + sample_file_content1 = dedent( + ''' + """This is a comment""" + + def a_function(foo: str, bar: int) -> None: + pass + ''' + ) + sample_file_content2 = dedent( + ''' + """This is a comment""" + + def b_function(foo: str, bar: int) -> None: + pass + ''' + ) + analyzer1 = get_prompt_analyzer(sample_file_content1) + analyzer2 = get_prompt_analyzer(sample_file_content2) + assert analyzer1.check_function_changed(analyzer2) + + +@pytest.mark.parametrize( + "from_imports", + [ + [], + [("mirascope", "tags", "mirascope_tag")], + [("mirascope", "tags", None)], + ], +) +@pytest.mark.parametrize( + "imports", + [ + [], + [("mirascope", None)], + [("mirascope", "ms")], + [("mirascope", "Mirascope"), ("fastapi", "FastAPI")], + ], +) +@pytest.mark.parametrize( + "class_decorators", + [ + ["tags(['movie_project'])"], # no version + ["tags(['version:0001', 'movie_project'])"], # version first + ["tags(['movie_project', 'version:0001'])"], # different version + ["tags(['movie_project', 'version:0002'])"], # same as revision_id + ["tags(['movie_project', 'version:0002', 'another_tag'])"], # tag in middle + [""], # no tags + ["tags(['version:0001', 'version:0002'])"], # two versions + ["mirascope.tags(['movie_project', 'version:0001'])"], # different import + ["ms.tags(['movie_project', 'version:0001'])"], # different import + ["tags()"], # improper tags + ], +) +@pytest.mark.parametrize( + "command, expected_variables", + [ + ( + MirascopeCommand.ADD, + MirascopeCliVariables(prev_revision_id="0001", revision_id="0002"), + ), + (MirascopeCommand.USE, None), + ], +) +@patch("mirascope_cli.utils.get_user_mirascope_settings") +@patch.object(Environment, "get_template") +@patch("mirascope_cli.utils.get_prompt_analyzer") +def test_write_prompt_to_template( + mock_prompt_analyzer: MagicMock, + mock_get_template: MagicMock, + mock_settings: MagicMock, + command: Literal[MirascopeCommand.ADD, MirascopeCommand.USE], + expected_variables: MirascopeCliVariables, + class_decorators: list[str], + imports: list[tuple[str, Optional[str]]], + from_imports: list[tuple[str, str, Optional[str]]], + fixture_mirascope_user_settings, +): + """Tests that a prompt is properly created from the template""" + + sample_file_content = "" + + mock_settings.return_value = fixture_mirascope_user_settings + + mock_template = MagicMock() + mock_template.render.return_value = _get_prompt_template() + mock_get_template.return_value = mock_template + prompt_analyzer = PromptAnalyzer() + prompt_analyzer.imports = imports + prompt_analyzer.from_imports = from_imports + prompt_analyzer.comments = "A prompt for recommending movies of a particular genre." + prompt_analyzer.classes = [ + ClassInfo( + name="MovieRecommendationPrompt", + docstring="Please recommend a list of movies in the {genre} category.", + body="", + bases=["BasePrompt"], + decorators=class_decorators, + ), + ] + mock_prompt_analyzer.return_value = prompt_analyzer + + write_prompt_to_template(sample_file_content, command, expected_variables) + + mock_settings.assert_called_once() + mock_get_template.assert_called_with("prompt_template.j2") + + assert mock_template.render.call_args is not None + + +def test_find_file_names(tmp_path: Path): + """Tests that the file names are properly found""" + foo_dir = tmp_path / "foo" + foo_dir.mkdir() + (foo_dir / "public_foo.py").write_text("foo") + (foo_dir / "_private.py").write_text("foo private") + + file_names = find_file_names(str(foo_dir)) + assert file_names == ["public_foo.py"] + + +@patch("mirascope_cli.utils.get_user_mirascope_settings") +def test_prompts_directory_files( + get_user_mirascope_settings: MagicMock, + fixture_mirascope_user_settings: MirascopeSettings, + tmp_path: Path, +): + """Tests that the prompts directory is properly found""" + os.chdir(tmp_path) + get_user_mirascope_settings.return_value = fixture_mirascope_user_settings + prompts_directory = tmp_path / fixture_mirascope_user_settings.prompts_location + prompts_directory.mkdir() + (prompts_directory / "public_foo.py").write_text("foo") + (prompts_directory / "_private.py").write_text("foo private") + + file_names = prompts_directory_files() + assert file_names == ["public_foo"] + + +def test_parse_prompt_file_name(): + """Tests that the prompt file name is properly parsed""" + prompt_file_name_with_extension = "0001_simple_prompt.py" + prompt_file_name = "0001_simple_prompt" + assert parse_prompt_file_name(prompt_file_name_with_extension) == prompt_file_name + assert parse_prompt_file_name(prompt_file_name) == prompt_file_name