diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bd656030..f6b5c135 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -200,6 +200,8 @@ jobs: spec: bin/vuforia-cloud-reco.py - name: vws-linux spec: bin/vuforia-web-services.py + - name: vumark-linux + spec: bin/vumark.py permissions: contents: write @@ -348,12 +350,24 @@ jobs: upload_exe_with_name: vws-windows clean_checkout: false + - name: Create Windows binary for VuMark generation + uses: sayyid5416/pyinstaller@v1 + with: + python_ver: '3.13' + pyinstaller_ver: ==6.12.0 + spec: bin/vumark.py + requirements: requirements.txt + options: --onefile, --name "vumark-windows" + upload_exe_with_name: vumark-windows + clean_checkout: false + - name: Upload Windows binaries to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: |- gh release upload ${{ needs.build.outputs.new_tag }} dist/vws-windows.exe --clobber gh release upload ${{ needs.build.outputs.new_tag }} dist/vuforia-cloud-reco-windows.exe --clobber + gh release upload ${{ needs.build.outputs.new_tag }} dist/vumark-windows.exe --clobber build-macos: name: Build macOS binaries @@ -408,12 +422,24 @@ jobs: upload_exe_with_name: vws-macos clean_checkout: false + - name: Create macOS binary for VuMark generation + uses: sayyid5416/pyinstaller@v1 + with: + python_ver: '3.13' + pyinstaller_ver: ==6.12.0 + spec: bin/vumark.py + requirements: requirements.txt + options: --onefile, --name "vumark-macos" + upload_exe_with_name: vumark-macos + clean_checkout: false + - name: Upload macOS binaries to release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: |- gh release upload ${{ needs.build.outputs.new_tag }} dist/vws-macos --clobber gh release upload ${{ needs.build.outputs.new_tag }} dist/vuforia-cloud-reco-macos --clobber + gh release upload ${{ needs.build.outputs.new_tag }} dist/vumark-macos --clobber publish-to-winget: name: Publish to WinGet @@ -423,6 +449,9 @@ jobs: contents: read steps: + # The first PR for a new package ID must be created manually on + # microsoft/winget-pkgs. We intentionally do not add vumark here yet. + # Tracked in https://github.com/VWS-Python/vws-cli/issues/1984. - uses: vedantmgoyal9/winget-releaser@v2 with: identifier: VWSPython.vws-cli diff --git a/README.rst b/README.rst index 624fe1ec..034ceb7a 100644 --- a/README.rst +++ b/README.rst @@ -69,6 +69,12 @@ To use ``vuforia-cloud-reco``: $ docker run --rm --entrypoint vuforia-cloud-reco "ghcr.io/vws-python/vws-cli" --help +To use ``vumark``: + +.. code-block:: console + + $ docker run --rm --entrypoint vumark "ghcr.io/vws-python/vws-cli" --help + With winget (Windows) ^^^^^^^^^^^^^^^^^^^^^ @@ -92,6 +98,11 @@ Pre-built Windows binaries ^^^^^^^^^^^^^^^^^^^^^^^^^^ Download the Windows executables from the `latest release`_ and place them in a directory on your ``PATH``. +The filenames are: + +* ``vws-windows.exe`` +* ``vuforia-cloud-reco-windows.exe`` +* ``vumark-windows.exe`` .. _latest release: https://github.com/VWS-Python/vws-cli/releases/latest diff --git a/bin/vumark.py b/bin/vumark.py new file mode 100755 index 00000000..9f1ee02d --- /dev/null +++ b/bin/vumark.py @@ -0,0 +1,7 @@ +#!/usr/bin/env python3 + +"""Run VuMark generation CLI.""" + +from vws_cli.vumark import generate_vumark + +generate_vumark() diff --git a/docs/source/commands.rst b/docs/source/commands.rst index 567b5ffd..27cc816d 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -8,3 +8,6 @@ Commands .. click:: vws_cli.query:vuforia_cloud_reco :prog: vuforia-cloud-reco :show-nested: + +.. click:: vws_cli.vumark:generate_vumark + :prog: vumark diff --git a/docs/source/install.rst b/docs/source/install.rst index 157d0eaf..44de4ec1 100644 --- a/docs/source/install.rst +++ b/docs/source/install.rst @@ -61,6 +61,13 @@ To use ``vuforia-cloud-reco``: $ docker run --rm --entrypoint vuforia-cloud-reco "|docker-image|" --help +To use ``vumark``: + +.. code-block:: console + :substitutions: + + $ docker run --rm --entrypoint vumark "|docker-image|" --help + With winget (Windows) ~~~~~~~~~~~~~~~~~~~~~ @@ -83,11 +90,18 @@ Pre-built Linux (x86) binaries chmod +x /usr/local/bin/vws $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vuforia-cloud-reco-linux" -o /usr/local/bin/vuforia-cloud-reco && chmod +x /usr/local/bin/vuforia-cloud-reco + $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vumark-linux" -o /usr/local/bin/vumark && + chmod +x /usr/local/bin/vumark Pre-built Windows binaries ~~~~~~~~~~~~~~~~~~~~~~~~~~ Download the Windows executables from the `latest release`_ and place them in a directory on your ``PATH``. +The filenames are: + +* ``vws-windows.exe`` +* ``vuforia-cloud-reco-windows.exe`` +* ``vumark-windows.exe`` .. _latest release: https://github.com/VWS-Python/vws-cli/releases/latest @@ -101,6 +115,8 @@ Pre-built macOS (ARM) binaries chmod +x /usr/local/bin/vws $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vuforia-cloud-reco-macos" -o /usr/local/bin/vuforia-cloud-reco && chmod +x /usr/local/bin/vuforia-cloud-reco + $ curl --fail -L "https://github.com/|github-owner|/|github-repository|/releases/download/|release|/vumark-macos" -o /usr/local/bin/vumark && + chmod +x /usr/local/bin/vumark You may need to remove the quarantine attribute to allow the binaries to run: @@ -108,6 +124,7 @@ You may need to remove the quarantine attribute to allow the binaries to run: $ xattr -d com.apple.quarantine /usr/local/bin/vws $ xattr -d com.apple.quarantine /usr/local/bin/vuforia-cloud-reco + $ xattr -d com.apple.quarantine /usr/local/bin/vumark Shell completion ~~~~~~~~~~~~~~~~ diff --git a/docs/source/release-process.rst b/docs/source/release-process.rst index ef379676..1bf16f4b 100644 --- a/docs/source/release-process.rst +++ b/docs/source/release-process.rst @@ -8,7 +8,9 @@ Outcomes * A new package on PyPI. * A new Homebrew recipe available to install. * A new Docker image on GitHub Container Registry. -* New Winget packages available to install. +* New binary assets attached to the GitHub release. +* New Winget packages available to install for ``vws`` and + ``vuforia-cloud-reco``. Perform a Release ~~~~~~~~~~~~~~~~~ @@ -23,3 +25,11 @@ Perform a Release $ gh workflow run release.yml --repo "|github-owner|/|github-repository|" .. _Install GitHub CLI: https://cli.github.com/ + +WinGet for ``vumark`` +~~~~~~~~~~~~~~~~~~~~~ + +The first WinGet PR for a new package ID must be created manually. +For ``vumark``, do this after the first release that contains +``vumark-windows.exe``, then automation can be added for subsequent releases. +This is tracked in `issue #1984 `_. diff --git a/pyproject.toml b/pyproject.toml index 84b6698d..952fe1f2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -82,7 +82,7 @@ optional-dependencies.dev = [ "ty==0.0.18", "types-pyyaml==6.0.12.20250915", "vulture==2.14", - "vws-python-mock==2026.2.21", + "vws-python-mock==2026.2.22.1", "vws-test-fixtures==2023.3.5", "yamlfix==1.19.1", "zizmor==1.22.0", @@ -94,6 +94,7 @@ optional-dependencies.release = [ urls.Documentation = "https://vws-python.github.io/vws-cli/" urls.Source = "https://github.com/VWS-Python/vws-cli" scripts.vuforia-cloud-reco = "vws_cli.query:vuforia_cloud_reco" +scripts.vumark = "vws_cli.vumark:generate_vumark" scripts.vws = "vws_cli:vws_group" [tool.setuptools] diff --git a/spelling_private_dict.txt b/spelling_private_dict.txt index 0288fa84..fda07bcc 100644 --- a/spelling_private_dict.txt +++ b/spelling_private_dict.txt @@ -20,6 +20,8 @@ macOS metadata noqa num +pdf +png pragma pre pyperclip @@ -31,10 +33,12 @@ reportMissingTypeStubs reportUnknownArgumentType reportUnknownMemberType reportUnknownVariableType +svg typeshed ubuntu versioned vuforia +vumark vwq vws winget diff --git a/src/vws_cli/vumark.py b/src/vws_cli/vumark.py new file mode 100644 index 00000000..e146bf27 --- /dev/null +++ b/src/vws_cli/vumark.py @@ -0,0 +1,151 @@ +"""``click`` command for VuMark generation.""" + +import contextlib +import sys +from collections.abc import Iterator +from enum import StrEnum, unique +from pathlib import Path + +import click +from beartype import beartype +from vws import VuMarkService +from vws.exceptions.base_exceptions import VWSError +from vws.exceptions.custom_exceptions import ServerError +from vws.exceptions.vws_exceptions import ( + InvalidInstanceIdError, + TargetStatusNotSuccessError, + UnknownTargetError, +) +from vws.vumark_accept import VuMarkAccept + +from vws_cli import __version__ +from vws_cli._error_handling import get_error_message +from vws_cli.options.credentials import ( + server_access_key_option, + server_secret_key_option, +) +from vws_cli.options.targets import target_id_option +from vws_cli.options.timeout import ( + connection_timeout_seconds_option, + read_timeout_seconds_option, +) +from vws_cli.options.vws import base_vws_url_option + + +@beartype +@unique +class VuMarkFormatChoice(StrEnum): + """Choices for the VuMark output format.""" + + PNG = "png" + SVG = "svg" + PDF = "pdf" + + +_FORMAT_CHOICE_TO_ACCEPT: dict[VuMarkFormatChoice, VuMarkAccept] = { + VuMarkFormatChoice.PNG: VuMarkAccept.PNG, + VuMarkFormatChoice.SVG: VuMarkAccept.SVG, + VuMarkFormatChoice.PDF: VuMarkAccept.PDF, +} + + +@beartype +@contextlib.contextmanager +def _handle_vumark_exceptions() -> Iterator[None]: + """Show error messages and catch exceptions from ``VWS-Python``.""" + error_message = "" + + try: + yield + except (UnknownTargetError, TargetStatusNotSuccessError) as exc: + if isinstance(exc, UnknownTargetError): + error_message = f'Error: Target "{exc.target_id}" does not exist.' + else: + error_message = ( + f'Error: The target "{exc.target_id}" is not in the success ' + "state and cannot be used to generate a VuMark instance." + ) + except (VWSError, ServerError) as exc: + if isinstance(exc, InvalidInstanceIdError): + error_message = "Error: The given instance ID is invalid." + else: + error_message = get_error_message(exc=exc) + else: + return + + click.echo(message=error_message, err=True) + sys.exit(1) + + +@click.command(name="vumark") +@server_access_key_option +@server_secret_key_option +@target_id_option +@click.option( + "--instance-id", + type=str, + required=True, + help="The instance ID to encode in the VuMark.", +) +@click.option( + "--format", + "format_choice", + type=click.Choice(choices=VuMarkFormatChoice, case_sensitive=False), + default=VuMarkFormatChoice.PNG.lower(), + help="The output format for the generated VuMark.", + show_default=True, +) +@click.option( + "--output", + "output_file_path", + type=click.Path( + dir_okay=False, + writable=True, + path_type=Path, + ), + required=True, + help="The path to write the generated VuMark to.", +) +@_handle_vumark_exceptions() +@base_vws_url_option +@connection_timeout_seconds_option +@read_timeout_seconds_option +@click.version_option(version=__version__) +@beartype +def generate_vumark( + *, + server_access_key: str, + server_secret_key: str, + target_id: str, + instance_id: str, + format_choice: VuMarkFormatChoice, + output_file_path: Path, + base_vws_url: str, + connection_timeout_seconds: float, + read_timeout_seconds: float, +) -> None: + """Generate a VuMark instance. + + \b + See + https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/ + """ + vumark_client = VuMarkService( + server_access_key=server_access_key, + server_secret_key=server_secret_key, + base_vws_url=base_vws_url, + request_timeout_seconds=( + connection_timeout_seconds, + read_timeout_seconds, + ), + ) + + accept = _FORMAT_CHOICE_TO_ACCEPT[format_choice] + + vumark_data = vumark_client.generate_vumark_instance( + target_id=target_id, + instance_id=instance_id, + accept=accept, + ) + + output_file_path.write_bytes(data=vumark_data) diff --git a/tests/conftest.py b/tests/conftest.py index 6d6385d4..18292a42 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,8 @@ import pytest from beartype import beartype from mock_vws import MockVWS -from mock_vws.database import CloudDatabase +from mock_vws.database import CloudDatabase, VuMarkDatabase +from mock_vws.target import VuMarkTarget from vws import VWS, CloudRecoService @@ -35,6 +36,22 @@ def vws_client(mock_database: CloudDatabase) -> VWS: ) +@pytest.fixture(name="vumark_database") +def fixture_vumark_database() -> Iterator[VuMarkDatabase]: + """Yield a mock ``VuMarkDatabase`` with one pre-created target.""" + vumark_target = VuMarkTarget(name="test-vumark-target") + database = VuMarkDatabase(vumark_targets={vumark_target}) + with MockVWS() as mock: + mock.add_vumark_database(vumark_database=database) + yield database + + +@pytest.fixture(name="vumark_target") +def fixture_vumark_target(vumark_database: VuMarkDatabase) -> VuMarkTarget: + """Return the pre-created ``VuMarkTarget`` in the database.""" + return next(iter(vumark_database.not_deleted_targets)) + + @pytest.fixture def cloud_reco_client( mock_database: CloudDatabase, diff --git a/tests/test_help.py b/tests/test_help.py index d37c8e18..a9e8647d 100644 --- a/tests/test_help.py +++ b/tests/test_help.py @@ -6,6 +6,7 @@ from vws_cli import vws_group from vws_cli.query import vuforia_cloud_reco +from vws_cli.vumark import generate_vumark _SUBCOMMANDS = [[item] for item in vws_group.commands] _BASE_COMMAND: list[list[str]] = [[]] @@ -39,6 +40,24 @@ def test_vws_command_help( file_regression.check(contents=result.output) +def test_vumark_help(file_regression: FileRegressionFixture) -> None: + """Expected help text is shown for the ``vumark`` command. + + This help text is defined in files. + To update these files, run ``pytest`` with the ``--regen-all`` flag. + """ + runner = CliRunner() + arguments = ["--help"] + result = runner.invoke( + cli=generate_vumark, + args=arguments, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + file_regression.check(contents=result.output) + + def test_query_help(file_regression: FileRegressionFixture) -> None: """Expected help text is shown for the ``vuforia-cloud-reco`` command. diff --git a/tests/test_help/test_vumark_help.txt b/tests/test_help/test_vumark_help.txt new file mode 100644 index 00000000..d5ab6ba8 --- /dev/null +++ b/tests/test_help/test_vumark_help.txt @@ -0,0 +1,32 @@ +Usage: vumark [OPTIONS] + + Generate a VuMark instance. + + See + https://developer.vuforia.com/library/vuforia-engine/web-api/vumark-generation-web-api/ + +Options: + --server-access-key TEXT A Vuforia server access key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_ACCESS_KEY; required] + --server-secret-key TEXT A Vuforia server secret key to use to access + the Vuforia Web Services API. [env var: + VUFORIA_SERVER_SECRET_KEY; required] + --target-id TEXT The ID of a target in the Vuforia database. + [required] + --instance-id TEXT The instance ID to encode in the VuMark. + [required] + --format [png|svg|pdf] The output format for the generated VuMark. + [default: png] + --output FILE The path to write the generated VuMark to. + [required] + --base-vws-url TEXT The base URL for the VWS API. [default: + https://vws.vuforia.com] + --connection-timeout-seconds FLOAT RANGE + The connection timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --read-timeout-seconds FLOAT RANGE + The read timeout for HTTP requests, in + seconds. [default: 30; x>=0.05] + --version Show the version and exit. + --help Show this message and exit. diff --git a/tests/test_vumark.py b/tests/test_vumark.py new file mode 100644 index 00000000..91136fa5 --- /dev/null +++ b/tests/test_vumark.py @@ -0,0 +1,269 @@ +"""Tests for the ``vumark`` CLI command.""" + +import uuid +from pathlib import Path + +import click +import pytest +from click.testing import CliRunner +from mock_vws import MockVWS +from mock_vws.database import CloudDatabase, VuMarkDatabase +from mock_vws.target import VuMarkTarget + +from vws_cli import vumark as vumark_module + +generate_vumark = vumark_module.generate_vumark + + +class TestGenerateVuMark: + """Tests for ``vumark``.""" + + @staticmethod + @pytest.mark.parametrize( + argnames=("format_name", "expected_prefix"), + argvalues=[ + pytest.param("png", b"\x89PNG\r\n\x1a\n", id="png"), + pytest.param("svg", b"<", id="svg"), + pytest.param("pdf", b"%PDF", id="pdf"), + ], + ) + def test_generate_vumark_format( + vumark_database: VuMarkDatabase, + vumark_target: VuMarkTarget, + tmp_path: Path, + format_name: str, + expected_prefix: bytes, + ) -> None: + """The returned file matches the requested format.""" + runner = CliRunner() + output_file = tmp_path / f"output.{format_name}" + commands = [ + "--target-id", + vumark_target.target_id, + "--instance-id", + "12345", + "--format", + format_name, + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + vumark_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + assert output_file.read_bytes().startswith(expected_prefix) + + @staticmethod + def test_default_format_is_png( + vumark_database: VuMarkDatabase, + vumark_target: VuMarkTarget, + tmp_path: Path, + ) -> None: + """The default output format is png.""" + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + vumark_target.target_id, + "--instance-id", + "12345", + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + vumark_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 0 + assert output_file.exists() + assert output_file.read_bytes().startswith(b"\x89PNG\r\n\x1a\n") + + @staticmethod + def test_unknown_target( + vumark_database: VuMarkDatabase, + tmp_path: Path, + ) -> None: + """An error is shown when the target ID does not exist.""" + nonexistent_target_id = uuid.uuid4().hex + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + nonexistent_target_id, + "--instance-id", + "12345", + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + vumark_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = ( + f'Error: Target "{nonexistent_target_id}" does not exist.\n' + ) + assert result.stderr == expected_stderr + + @staticmethod + def test_invalid_instance_id( + vumark_database: VuMarkDatabase, + vumark_target: VuMarkTarget, + tmp_path: Path, + ) -> None: + """An error is shown when the instance ID is invalid.""" + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + vumark_target.target_id, + "--instance-id", + "", + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + vumark_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = "Error: The given instance ID is invalid.\n" + assert result.stderr == expected_stderr + + @staticmethod + def test_target_not_in_success_state( + tmp_path: Path, + ) -> None: + """An error is shown when the target is not in the success + state. + """ + processing_target = VuMarkTarget( + name="processing-target", + processing_time_seconds=9999, + ) + database = VuMarkDatabase(vumark_targets={processing_target}) + with MockVWS() as mock: + mock.add_vumark_database(vumark_database=database) + target_id = processing_target.target_id + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + target_id, + "--instance-id", + "12345", + "--output", + str(object=output_file), + "--server-access-key", + database.server_access_key, + "--server-secret-key", + database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = ( + f'Error: The target "{target_id}" is not in the success ' + "state and cannot be used to generate a VuMark instance.\n" + ) + assert result.stderr == expected_stderr + + @staticmethod + def test_authentication_failure( + vumark_database: VuMarkDatabase, + vumark_target: VuMarkTarget, + tmp_path: Path, + ) -> None: + """An error is shown on authentication failure.""" + runner = CliRunner() + output_file = tmp_path / "output.png" + commands = [ + "--target-id", + vumark_target.target_id, + "--instance-id", + "12345", + "--output", + str(object=output_file), + "--server-access-key", + vumark_database.server_access_key, + "--server-secret-key", + "wrong-secret-key", + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == 1 + expected_stderr = "The given secret key was incorrect.\n" + assert result.stderr == expected_stderr + + +@pytest.mark.parametrize(argnames="invalid_format", argvalues=["bmp", "gif"]) +def test_invalid_format( + mock_database: CloudDatabase, + tmp_path: Path, + invalid_format: str, +) -> None: + """An error is shown for an unrecognized format choice.""" + runner = CliRunner() + output_file = tmp_path / "output" + commands = [ + "--target-id", + "some-target-id", + "--instance-id", + "12345", + "--format", + invalid_format, + "--output", + str(object=output_file), + "--server-access-key", + mock_database.server_access_key, + "--server-secret-key", + mock_database.server_secret_key, + ] + result = runner.invoke( + cli=generate_vumark, + args=commands, + catch_exceptions=False, + color=True, + ) + assert result.exit_code == click.UsageError.exit_code + expected_output = ( + "Usage: vumark [OPTIONS]\n" + "Try 'vumark --help' for help.\n" + "\n" + f"Error: Invalid value for '--format': '{invalid_format}' is not " + "one of 'png', 'svg', 'pdf'.\n" + ) + assert result.output == expected_output diff --git a/uv.lock b/uv.lock index fb0d4256..4506969b 100644 --- a/uv.lock +++ b/uv.lock @@ -42,6 +42,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, ] +[[package]] +name = "anyio" +version = "4.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/96/f0/5eb65b2bb0d09ac6776f2eb54adee6abe8228ea05b20a5ad0e4945de8aac/anyio-4.12.1.tar.gz", hash = "sha256:41cfcc3a4c85d3f05c932da7c26d0201ac36f72abd4435ba90d0464a3ffed703", size = 228685, upload-time = "2026-01-06T11:45:21.246Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/0e/27be9fdef66e72d64c0cdc3cc2823101b80585f8119b5c112c2e8f5f7dab/anyio-4.12.1-py3-none-any.whl", hash = "sha256:d405828884fc140aa80a3c667b8beed277f1dfedec42ba031bd6ac3db606ab6c", size = 113592, upload-time = "2026-01-06T11:45:19.497Z" }, +] + [[package]] name = "astroid" version = "4.0.3" @@ -493,6 +505,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/b2/50e9b292b5cac13e9e81272c7171301abc753a60460d21505b606e15cf21/furo-2025.12.19-py3-none-any.whl", hash = "sha256:bb0ead5309f9500130665a26bee87693c41ce4dbdff864dbfb6b0dae4673d24f", size = 339262, upload-time = "2025-12-19T17:34:38.905Z" }, ] +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + [[package]] name = "hadolint-bin" version = "2.14.0" @@ -517,6 +538,34 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/3b/85/998232eae0b5c6798c7140ef37d2c1be02ea06cd38dd80169b3abd63b600/homebrew_pypi_poet-0.10.0-py2.py3-none-any.whl", hash = "sha256:65824f97aea0e713c4ac18aa2ef4477aca69426554eac842eeaaddf97df3fc47", size = 7813, upload-time = "2018-02-23T06:22:54.858Z" }, ] +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -1545,6 +1594,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/4c/cc276ce57e572c102d9542d383b2cfd551276581dc60004cb94fe8774c11/responses-0.25.8-py3-none-any.whl", hash = "sha256:0c710af92def29c8352ceadff0c3fe340ace27cf5af1bbe46fb71275bcd2831c", size = 34769, upload-time = "2025-08-08T19:01:45.018Z" }, ] +[[package]] +name = "respx" +version = "0.22.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/7c/96bd0bc759cf009675ad1ee1f96535edcb11e9666b985717eb8c87192a95/respx-0.22.0.tar.gz", hash = "sha256:3c8924caa2a50bd71aefc07aa812f2466ff489f1848c96e954a5362d17095d91", size = 28439, upload-time = "2024-12-19T22:33:59.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/67/afbb0978d5399bc9ea200f1d4489a23c9a1dad4eee6376242b8182389c79/respx-0.22.0-py2.py3-none-any.whl", hash = "sha256:631128d4c9aba15e56903fb5f66fb1eff412ce28dd387ca3a81339e52dbd3ad0", size = 25127, upload-time = "2024-12-19T22:33:57.837Z" }, +] + [[package]] name = "restructuredtext-lint" version = "2.0.2" @@ -2336,7 +2397,7 @@ requires-dist = [ { name = "types-pyyaml", marker = "extra == 'dev'", specifier = "==6.0.12.20250915" }, { name = "vulture", marker = "extra == 'dev'", specifier = "==2.14" }, { name = "vws-python", specifier = "==2026.2.21" }, - { name = "vws-python-mock", marker = "extra == 'dev'", specifier = "==2026.2.21" }, + { name = "vws-python-mock", marker = "extra == 'dev'", specifier = "==2026.2.22.1" }, { name = "vws-test-fixtures", marker = "extra == 'dev'", specifier = "==2023.3.5" }, { name = "yamlfix", marker = "extra == 'dev'", specifier = "==1.19.1" }, { name = "zizmor", marker = "extra == 'dev'", specifier = "==1.22.0" }, @@ -2360,17 +2421,19 @@ wheels = [ [[package]] name = "vws-python-mock" -version = "2026.2.21" +version = "2026.2.22.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "beartype" }, { name = "flask" }, + { name = "httpx" }, { name = "numpy" }, { name = "pillow" }, { name = "piq" }, { name = "pydantic-settings" }, { name = "requests" }, { name = "responses" }, + { name = "respx" }, { name = "torch", version = "2.10.0", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform == 'darwin'" }, { name = "torch", version = "2.10.0+cpu", source = { registry = "https://download.pytorch.org/whl/cpu" }, marker = "sys_platform != 'darwin'" }, { name = "torchmetrics" }, @@ -2380,9 +2443,9 @@ dependencies = [ { name = "vws-auth-tools" }, { name = "werkzeug" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a8/c6/73e35a2c9390c2db650083c22db86e1b160b78751130d410d08a1667306d/vws_python_mock-2026.2.21.tar.gz", hash = "sha256:173d901643b8ea9fc643aad3289330d4bb3a3525a11eeb3c89b268e7c4d6b26e", size = 158586, upload-time = "2026-02-21T00:17:16.08Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b4/89/e0e4a90cfbbe6af5d6c4a04ecff5fb76335169c896336ab1d753012fcc01/vws_python_mock-2026.2.22.1.tar.gz", hash = "sha256:85f29f1d172a8c83699e518cd4e8d2dc3e680e8445f3cf2d0cb8adcd8a2d1918", size = 165249, upload-time = "2026-02-22T11:56:56.979Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/90/0e/67e79a88cd9bfb02d7efc20c9270dd82c02af68376ca19d30cca7f119bf7/vws_python_mock-2026.2.21-py2.py3-none-any.whl", hash = "sha256:51b24439f8ee580ff3848cc86fc9297e23f3cc6845c14af691346dad6e3ee51a", size = 67491, upload-time = "2026-02-21T00:17:14.17Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a6/7cd9ee2558247ff319ff26a985d6668023d650a5f0326a59fbc2ee899b49/vws_python_mock-2026.2.22.1-py2.py3-none-any.whl", hash = "sha256:4c04f684869ef1f81800e60b7357476fc133f9e3d277ed8d106a404499480464", size = 70914, upload-time = "2026-02-22T11:56:55.356Z" }, ] [[package]]