Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9557e6d
Add VuMark instance generation CLI command
adamtheturtle Feb 19, 2026
c6e1cce
Update VuMark implementation to match vws-python#2858
adamtheturtle Feb 19, 2026
d878070
Parametrize format tests in test_vumark
adamtheturtle Feb 19, 2026
c0e55a9
Fix mypy errors for not-yet-available VWS method
adamtheturtle Feb 21, 2026
b78ce9d
Use VuMarkService instead of VWS for generate_vumark_instance
adamtheturtle Feb 21, 2026
3d8c3f8
Fix pylint errors in test_vumark
adamtheturtle Feb 21, 2026
26d1aef
Add VuMark-related words to spelling private dictionary
adamtheturtle Feb 21, 2026
d8f51fe
Make VuMark a standalone binary instead of a vws subcommand
adamtheturtle Feb 21, 2026
de61662
Mark VuMark tests as xfail where mock-vws lacks support
adamtheturtle Feb 21, 2026
9a0645c
test: replace VuforiaDatabase usage with CloudDatabase
adamtheturtle Feb 21, 2026
35c392e
Add vumark binary release support and fix vumark error handling
adamtheturtle Feb 21, 2026
18fbed1
Fix mypy typing for vumark error message fallback
adamtheturtle Feb 21, 2026
a9d9199
Fix pyright issues in vumark error mapping and tests
adamtheturtle Feb 21, 2026
d990a76
test: refactor vumark tests to use monkeypatching instead of real API…
adamtheturtle Feb 21, 2026
74b7463
fix: use VuMarkService from vws directly for monkeypatching in tests
adamtheturtle Feb 21, 2026
bd396f9
fix: handle InvalidTargetTypeError in vumark error message mapping
adamtheturtle Feb 21, 2026
2f1b198
test: convert test_unknown_target to use monkeypatching instead of xfail
adamtheturtle Feb 21, 2026
d431ce1
test: simplify vumark tests to use mock-vws directly
adamtheturtle Feb 21, 2026
2f90368
test: remove unused vumark_client fixture to restore 100% coverage
adamtheturtle Feb 22, 2026
a506270
fix: restore pytest ini_options and disable capture for Click compati…
adamtheturtle Feb 22, 2026
4da5c7a
refactor: inline _get_vumark_error_message and test at integration level
adamtheturtle Feb 22, 2026
6cefd3f
fix: suppress pylint no-member for exc.target_id after isinstance nar…
adamtheturtle Feb 22, 2026
f13f4ce
fix: rename _get_error_message to get_error_message to satisfy pyright
adamtheturtle Feb 22, 2026
38c41cc
refactor: replace isinstance narrowing with two except clauses in vumark
adamtheturtle Feb 22, 2026
112b0b0
test: assert on actual output in test_invalid_format
adamtheturtle Feb 22, 2026
6d1bd9b
revert: reset pyproject.toml to match main
adamtheturtle Feb 22, 2026
fb97257
fix: remove unused imports from vumark.py
adamtheturtle Feb 22, 2026
a8c6497
test: replace xfail with monkeypatched tests and remove pragma no cover
adamtheturtle Feb 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
11 changes: 11 additions & 0 deletions README.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing scripts.vumark entry in pyproject.toml breaks Docker

High Severity

The documentation tells users to run docker run --rm --entrypoint vumark ..., but pyproject.toml is missing a scripts.vumark entry (only scripts.vws and scripts.vuforia-cloud-reco exist). The Docker image installs the package via uv pip install, which only creates console scripts declared in pyproject.toml. Without scripts.vumark = "vws_cli.vumark:generate_vumark", the vumark command won't exist in the Docker image or any pip-based installation, making all the Docker documentation and pip usage broken.

Additional Locations (1)

Fix in Cursor Fix in Web


With winget (Windows)
^^^^^^^^^^^^^^^^^^^^^

Expand All @@ -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

Expand Down
7 changes: 7 additions & 0 deletions bin/vumark.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
#!/usr/bin/env python3

"""Run VuMark generation CLI."""

from vws_cli.vumark import generate_vumark

generate_vumark()
3 changes: 3 additions & 0 deletions docs/source/commands.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
17 changes: 17 additions & 0 deletions docs/source/install.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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)
~~~~~~~~~~~~~~~~~~~~~

Expand All @@ -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

Expand All @@ -101,13 +115,16 @@ 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:

.. code-block:: console

$ 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
~~~~~~~~~~~~~~~~
Expand Down
12 changes: 11 additions & 1 deletion docs/source/release-process.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
~~~~~~~~~~~~~~~~~
Expand All @@ -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 <https://github.com/VWS-Python/vws-cli/issues/1984>`_.
4 changes: 4 additions & 0 deletions spelling_private_dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ macOS
metadata
noqa
num
pdf
png
pragma
pre
pyperclip
Expand All @@ -31,10 +33,12 @@ reportMissingTypeStubs
reportUnknownArgumentType
reportUnknownMemberType
reportUnknownVariableType
svg
typeshed
ubuntu
versioned
vuforia
vumark
vwq
vws
winget
151 changes: 151 additions & 0 deletions src/vws_cli/vumark.py
Original file line number Diff line number Diff line change
@@ -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)
19 changes: 18 additions & 1 deletion tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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,
Expand Down
Loading