Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
5 changes: 4 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ optional-dependencies.dev = [
"towncrier==26.9.0",
"ty==0.0.78",
"types-pyyaml==6.0.12.20260815",
"types-requests==2.33.0.20260712",
"vale==3.20.0.0",
"vulture==2.16",
"vws-python-mock==2026.8.26.1",
Expand Down Expand Up @@ -357,6 +358,8 @@ exclude = [
# Ideally we would limit the paths to the source code where we want to ignore names,
# but Vulture does not enable this.
ignore_names = [
# Structural Protocol member used through Click decorator objects.
"__call__",
# Sphinx
"autoclass_content",
"autoclass_content",
Expand Down Expand Up @@ -410,7 +413,7 @@ plugins = [

[tool.pyrefly]
errors.non-exhaustive-match = "error"
preset = "strict"
preset = "all"

[tool.pyright]
typeCheckingMode = "strict"
Expand Down
1 change: 1 addition & 0 deletions spelling_private_dict.txt
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ png
pragma
pre
pyperclip
pyrefly
pyright
pytest
reco
Expand Down
11 changes: 9 additions & 2 deletions src/vws_cli/_error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,12 +122,19 @@ def get_model_target_error_message(
case ModelTargetValidationError():
problems = [
f"{detail.code}: {detail.message}" for detail in exc.details
] or [exc.message]
]
if len(problems) == 0:
problems = [exc.message]
message = "\n".join(
["Error: Vuforia rejected the request.", *problems],
)
case ModelTargetError():
message = f"Error: {exc.message or 'Vuforia returned an error.'}"
error_message = (
exc.message
if exc.message != ""
else "Vuforia returned an error."
)
message = f"Error: {error_message}"
case _:
message = get_error_message(exc=exc)

Expand Down
7 changes: 4 additions & 3 deletions src/vws_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -252,8 +252,9 @@ def get_target_summary_report(
)
report = vws_client.get_target_summary_report(target_id=target_id)
report_dict = dataclasses.asdict(obj=report)
report_dict["status"] = report_dict["status"].value
report_dict["upload_date"] = str(object=report_dict["upload_date"])
report_dict["status"] = report.status.value
upload_date: object = report_dict["upload_date"]
report_dict["upload_date"] = str(object=upload_date)
yaml_summary_report = yaml.dump(data=report_dict)
click.echo(message=yaml_summary_report)

Expand Down Expand Up @@ -650,4 +651,4 @@ def get_database_reco_counts_report(
click.echo(message=report.raw_csv, nl=False)
return

output_file_path.write_bytes(data=report.raw_csv)
_ = output_file_path.write_bytes(data=report.raw_csv)
26 changes: 14 additions & 12 deletions src/vws_cli/model_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,19 +171,21 @@ def _is_json_array(*, value: object) -> bool:


@beartype
def _as_json_object(*, value: object) -> dict[str, Any] | None:
def _as_json_object(*, value: object) -> dict[str, Any] | None: # pyrefly: ignore[explicit-any]
"""Get an object from a models file, or ``None``."""
if not _is_json_object(value=value):
return None
# The value goes through a variable which is typed as ``Any`` so that
# the keys and values of the returned object are not unknown types.
value_any: Any = value
value_dict: dict[str, Any] = value_any
value_any: Any = value # pyrefly: ignore[explicit-any]
value_dict: dict[str, Any] = ( # pyrefly: ignore[explicit-any]
value_any
)
return value_dict


@beartype
def _json_object(*, value: object, message: str) -> dict[str, Any]:
def _json_object(*, value: object, message: str) -> dict[str, Any]: # pyrefly: ignore[explicit-any]
"""Get an object from a models file, or raise an error."""
value_dict = _as_json_object(value=value)
if value_dict is None:
Expand All @@ -192,14 +194,14 @@ def _json_object(*, value: object, message: str) -> dict[str, Any]:


@beartype
def _json_array(*, value: object, message: str) -> list[Any]:
def _json_array(*, value: object, message: str) -> list[Any]: # pyrefly: ignore[explicit-any]
"""Get an array from a models file, or raise an error."""
if not _is_json_array(value=value):
raise _models_file_error(message=message)
# The value goes through a variable which is typed as ``Any`` so that
# the items of the returned array are not unknown types.
value_any: Any = value
value_list: list[Any] = value_any
value_any: Any = value # pyrefly: ignore[explicit-any]
value_list: list[Any] = value_any # pyrefly: ignore[explicit-any]
return value_list


Expand All @@ -210,14 +212,14 @@ def _checked_object(
known_fields: frozenset[str],
required_fields: Sequence[str],
path: str,
) -> dict[str, Any]:
) -> dict[str, Any]: # pyrefly: ignore[explicit-any]
"""Get an object with known and required fields, or raise an error."""
value_dict = _json_object(
value=value,
message=f"{path} must be an object.",
)
unknown_fields = sorted(set(value_dict) - known_fields)
if unknown_fields:
if bool(unknown_fields):
message = f"{path} has unknown fields: {', '.join(unknown_fields)}."
raise _models_file_error(message=message)

Expand Down Expand Up @@ -333,7 +335,7 @@ def _model_from_json(*, value: object, path: str) -> ModelTargetModel:
path=path,
)

model_kwargs: dict[str, Any] = {
model_kwargs: dict[str, Any] = { # pyrefly: ignore[explicit-any]
field_name: _string_value(
value=model_dict[json_field],
path=f"{path}/{json_field}",
Expand Down Expand Up @@ -566,7 +568,7 @@ def create_model_target_dataset(
for option_name, value in model_option_values.items()
if value is not None
)
if given_model_options:
if bool(given_model_options):
message = (
"--models-file cannot be used with "
f"{', '.join(given_model_options)}."
Expand Down Expand Up @@ -801,7 +803,7 @@ def download_model_target_dataset(
dataset_type=dataset_type,
)

output_file_path.write_bytes(data=dataset)
_ = output_file_path.write_bytes(data=dataset)


@click.command(name="delete-model-target-dataset")
Expand Down
17 changes: 17 additions & 0 deletions src/vws_cli/options/_types.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
"""Shared typing helpers for Click option decorators."""

from collections.abc import Callable
from typing import Protocol, runtime_checkable


@runtime_checkable
class ClickOptionDecorator(Protocol):
"""A decorator which preserves a command's signature."""

def __call__[**P, R](
self,
command: Callable[P, R],
/,
) -> Callable[P, R]:
"""Decorate ``command`` without changing its signature."""
raise NotImplementedError # pragma: no cover
25 changes: 12 additions & 13 deletions src/vws_cli/options/credentials.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"""``click`` options regarding credentials."""

from collections.abc import Callable
from typing import Any

import click
from beartype import beartype


@beartype
def server_access_key_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def server_access_key_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the Vuforia server access key."""
return click.option(
"--server-access-key",
Expand All @@ -26,9 +25,9 @@ def server_access_key_option(


@beartype
def server_secret_key_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def server_secret_key_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the Vuforia server secret key."""
return click.option(
"--server-secret-key",
Expand All @@ -44,9 +43,9 @@ def server_secret_key_option(


@beartype
def client_access_key_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def client_access_key_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the Vuforia client access key."""
return click.option(
"--client-access-key",
Expand All @@ -62,9 +61,9 @@ def client_access_key_option(


@beartype
def client_secret_key_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def client_secret_key_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the Vuforia client secret key."""
return click.option(
"--client-secret-key",
Expand Down
25 changes: 12 additions & 13 deletions src/vws_cli/options/model_targets.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
"""``click`` options regarding Model Target datasets."""

from collections.abc import Callable
from typing import Any

import click
from beartype import beartype
from vws.model_target_datasets import ModelTargetDatasetType


@beartype
def client_id_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def client_id_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the Model Target Web API client ID."""
return click.option(
"--client-id",
Expand All @@ -27,9 +26,9 @@ def client_id_option(


@beartype
def client_secret_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def client_secret_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the Model Target Web API client secret."""
return click.option(
"--client-secret",
Expand All @@ -45,9 +44,9 @@ def client_secret_option(


@beartype
def dataset_type_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def dataset_type_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the kind of Model Target dataset."""
return click.option(
"--dataset-type",
Expand All @@ -66,9 +65,9 @@ def dataset_type_option(


@beartype
def dataset_uuid_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def dataset_uuid_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the UUID of a Model Target dataset."""
return click.option(
"--dataset-uuid",
Expand Down
18 changes: 9 additions & 9 deletions src/vws_cli/options/targets.py
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
"""``click`` options regarding targets."""

from collections.abc import Callable
from enum import Enum, unique
from pathlib import Path
from typing import Any

import click
from beartype import beartype

target_id_option: Callable[..., Any] = click.option(
from vws_cli.options._types import ClickOptionDecorator

target_id_option: ClickOptionDecorator = click.option(
"--target-id",
type=str,
help="The ID of a target in the Vuforia database.",
Expand All @@ -17,7 +17,7 @@


@beartype
def target_name_option(*, required: bool) -> Callable[..., Any]:
def target_name_option(*, required: bool) -> ClickOptionDecorator:
"""An option decorator for choosing a target name."""
return click.option(
"--name",
Expand All @@ -28,9 +28,9 @@ def target_name_option(*, required: bool) -> Callable[..., Any]:


@beartype
def target_width_option(*, required: bool) -> Callable[..., Any]:
def target_width_option(*, required: bool) -> ClickOptionDecorator:
"""An option decorator for choosing a target width."""
option: Callable[..., Any] = click.option(
option: ClickOptionDecorator = click.option(
"--width",
type=float,
help="The width of the target in the Vuforia database.",
Expand All @@ -40,7 +40,7 @@ def target_width_option(*, required: bool) -> Callable[..., Any]:


@beartype
def target_image_option(*, required: bool) -> Callable[..., Any]:
def target_image_option(*, required: bool) -> ClickOptionDecorator:
"""An option decorator for choosing a target image."""
return click.option(
"--image",
Expand Down Expand Up @@ -68,7 +68,7 @@ class ActiveFlagChoice(Enum):
def active_flag_option(
*,
allow_none: bool,
) -> Callable[..., Any]:
) -> ClickOptionDecorator:
"""An option decorator for setting a target's active flag."""
if allow_none:
default = None
Expand All @@ -87,7 +87,7 @@ def active_flag_option(
)


application_metadata_option: Callable[..., Any] = click.option(
application_metadata_option: ClickOptionDecorator = click.option(
"--application-metadata",
type=str,
required=False,
Expand Down
13 changes: 6 additions & 7 deletions src/vws_cli/options/timeout.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
"""``click`` options regarding timeouts."""

from collections.abc import Callable
from typing import Any

import click
from beartype import beartype


@beartype
def connection_timeout_seconds_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def connection_timeout_seconds_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the connection timeout."""
return click.option(
"--connection-timeout-seconds",
Expand All @@ -22,9 +21,9 @@ def connection_timeout_seconds_option(


@beartype
def read_timeout_seconds_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
def read_timeout_seconds_option[**P, R](
command: Callable[P, R],
) -> Callable[P, R]:
"""An option decorator for the read timeout."""
return click.option(
"--read-timeout-seconds",
Expand Down
Loading
Loading