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
2 changes: 2 additions & 0 deletions newsfragments/2487.change
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Handle all VWS and Cloud Reco service errors with clear CLI messages, and use
consistent messages for target status and polling timeout errors.
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ optional-dependencies.dev = [
"types-pyyaml==6.0.12.20260815",
"vale==3.17.1.0",
"vulture==2.16",
"vws-python-mock==2026.8.14",
"vws-python-mock==2026.8.26",
"vws-test-fixtures==2026.8.23",
"yamlfix==1.19.1",
"zizmor==1.29.0",
Expand Down
28 changes: 23 additions & 5 deletions src/vws_cli/_error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,20 +17,25 @@
)
from vws.exceptions.vws_exceptions import (
AuthenticationFailureError,
AuthorizationFailedError,
BadImageError,
DateRangeError,
FailError,
ImageTooLargeError,
InvalidTargetTypeError,
LicenseCheckFailedError,
MetadataTooLargeError,
ProjectHasNoAPIAccessError,
ProjectInactiveError,
ProjectSuspendedError,
QuotaExceededError,
RequestQuotaReachedError,
RequestTimeTooSkewedError,
TargetNameExistError,
TargetQuotaReachedError,
TargetStatusNotSuccessError,
TargetStatusProcessingError,
TooManyRequestsError,
UnknownTargetError,
)

Expand All @@ -40,19 +45,24 @@ def get_error_message(exc: Exception) -> str:
"""Get an error message from a VWS exception."""
exc_type_to_message: dict[type[Exception], str] = {
AuthenticationFailureError: "The given secret key was incorrect.",
AuthorizationFailedError: "Error: The request was not authorized.",
BadImageError: "Error: The given image is corrupted or the format is not supported.",
DateRangeError: "Error: There was a problem with the date details given in the request.",
FailError: "Error: The request made to Vuforia was invalid and could not be processed. Check the given parameters.",
ImageTooLargeError: "Error: The given image is too large.",
InvalidTargetTypeError: "Error: The target type is invalid.",
LicenseCheckFailedError: "Error: The Vuforia license check failed.",
MetadataTooLargeError: "Error: The given metadata is too large.",
RecoCountsReportDownloadError: "Error: The recognition counts report could not be downloaded. This may be because the report's URL has expired.",
RecoCountsReportTimeoutError: "Error: The recognition counts report was not generated within the allowed limit.",
ServerError: "Error: There was an unknown error from Vuforia. This may be because there is a problem with the given name.",
ProjectInactiveError: "Error: The project associated with the given keys is inactive.",
QuotaExceededError: "Error: The request quota has been exceeded.",
RequestQuotaReachedError: "Error: The maximum number of API calls for this database has been reached.",
RequestTimeTooSkewedError: "Error: Vuforia reported that the time given with this request was outside the expected range. This may be because the system clock is out of sync.",
TargetProcessingTimeoutError: "Error: The target processing time has exceeded the allowed limit.",
TargetQuotaReachedError: "Error: The maximum number of targets for this database has been reached.",
TooManyRequestsError: "Error: Too many requests were made to Vuforia. Try again later.",
ProjectSuspendedError: "Error: The request could not be completed because this database has been suspended.",
ProjectHasNoAPIAccessError: "Error: The request could not be completed because this database is not allowed to make API requests.",
}
Expand All @@ -67,15 +77,18 @@ def get_error_message(exc: Exception) -> str:
case TargetStatusNotSuccessError():
return (
f'Error: The target "{exc.target_id}" cannot be updated as it is '
"in the processing state."
"not in the success state."
)
case TargetStatusProcessingError():
return (
f'Error: The target "{exc.target_id}" cannot be deleted as it is '
"in the processing state."
)
case _:
return exc_type_to_message[type(exc)]
return exc_type_to_message.get(
type(exc),
"Error: Vuforia returned an unrecognized error.",
)


@beartype
Expand All @@ -89,7 +102,12 @@ def get_model_target_error_message(
"Error: The given client ID and client secret are not a set "
"of Model Target Web API credentials."
)
case ModelTargetAuthenticationError():
# These fallbacks are retained for responses which the public mock
# cannot produce. Configurable failures and client coverage are
# tracked upstream in:
# https://github.com/VWS-Python/vws-python-mock/issues/3495
# https://github.com/VWS-Python/vws-python/issues/3169
case ModelTargetAuthenticationError(): # pragma: no cover
message = "Error: The request to Vuforia was not authenticated."
case UnknownModelTargetDatasetError():
message = (
Expand All @@ -108,9 +126,9 @@ def get_model_target_error_message(
message = "\n".join(
["Error: Vuforia rejected the request.", *problems],
)
case ModelTargetError():
case ModelTargetError(): # pragma: no cover
message = f"Error: {exc.message or 'Vuforia returned an error.'}"
case _:
case _: # pragma: no cover
message = get_error_message(exc=exc)

return message
55 changes: 15 additions & 40 deletions src/vws_cli/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
ServerError,
TargetProcessingTimeoutError,
)
from vws.exceptions.vws_exceptions import AuthenticationFailureError

from vws_cli._error_handling import get_error_message
from vws_cli.options.credentials import (
Expand Down Expand Up @@ -55,6 +54,7 @@ def _handle_vws_exceptions() -> Generator[None]:
except (
VWSError,
RecoCountsReportDownloadError,
RecoCountsReportTimeoutError,
ServerError,
TargetProcessingTimeoutError,
) as exc:
Expand Down Expand Up @@ -483,18 +483,11 @@ def wait_for_target_processed(
),
)

try:
vws_client.wait_for_target_processed(
target_id=target_id,
seconds_between_requests=seconds_between_requests,
timeout_seconds=timeout_seconds,
)
except TargetProcessingTimeoutError:
click.echo(
message=f"Timeout of {timeout_seconds} seconds reached.",
err=True,
)
sys.exit(1)
vws_client.wait_for_target_processed(
target_id=target_id,
seconds_between_requests=seconds_between_requests,
timeout_seconds=timeout_seconds,
)


_MONTH_FORMAT = "%Y-%m"
Expand Down Expand Up @@ -637,38 +630,20 @@ def get_database_reco_counts_report(
),
)

try:
report_request = vws_client.request_database_reco_counts_report(
year=month.year,
month=calendar.Month(value=month.month),
)
except AuthenticationFailureError:
click.echo(
message=(
"Error: The given secret key was incorrect, or the given "
"database ID is not the ID of the database which the given "
"server keys belong to."
),
err=True,
)
sys.exit(1)
report_request = vws_client.request_database_reco_counts_report(
year=month.year,
month=calendar.Month(value=month.month),
)

if no_wait:
click.echo(message=report_request.presigned_url)
return

try:
report = vws_client.wait_for_reco_counts_report(
presigned_url=report_request.presigned_url,
seconds_between_requests=seconds_between_requests,
timeout_seconds=timeout_seconds,
)
except RecoCountsReportTimeoutError:
click.echo(
message=f"Timeout of {timeout_seconds} seconds reached.",
err=True,
)
sys.exit(1)
report = vws_client.wait_for_reco_counts_report(
presigned_url=report_request.presigned_url,
seconds_between_requests=seconds_between_requests,
timeout_seconds=timeout_seconds,
)

if output_file_path is None:
click.echo(message=report.raw_csv, nl=False)
Expand Down
2 changes: 2 additions & 0 deletions src/vws_cli/model_target.py
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ def _model_from_json(*, value: object, path: str) -> ModelTargetModel:
path=f"{path}/{json_field}",
)

model_kwargs["views"] = []
if "views" in model_dict:
views_items = _json_array(
value=model_dict["views"],
Expand Down Expand Up @@ -604,6 +605,7 @@ def create_model_target_dataset(
state_based_configuration_json_string=(
state_based_configuration_json_string
),
views=[],
),
]

Expand Down
52 changes: 33 additions & 19 deletions src/vws_cli/query.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import yaml
from beartype import beartype
from vws import CloudRecoService
from vws.exceptions.base_exceptions import CloudRecoError
from vws.exceptions.cloud_reco_exceptions import (
AuthenticationFailureError,
BadImageError,
Expand All @@ -19,6 +20,7 @@
)
from vws.exceptions.custom_exceptions import (
RequestEntityTooLargeError,
ServerError,
)
from vws.include_target_data import CloudRecoIncludeTargetData

Expand All @@ -33,31 +35,43 @@
)


@beartype
def _get_cloud_reco_error_message(exc: Exception) -> str:
"""Get an error message from a Cloud Reco exception."""
match exc:
case BadImageError():
message = (
"Error: The given image is corrupted or the format is not "
"supported."
)
case InactiveProjectError():
message = "Error: The project associated with the given keys is inactive."
case AuthenticationFailureError():
message = "The given secret key was incorrect."
case RequestTimeTooSkewedError():
message = (
"Error: Vuforia reported that the time given with this request "
"was outside the expected range. "
"This may be because the system clock is out of sync."
)
case RequestEntityTooLargeError():
message = "Error: The given image is too large."
case ServerError():
message = "Error: There was an unknown error from Vuforia."
case _:
message = "Error: Vuforia rejected the request."

return message


@beartype
@contextlib.contextmanager
def _handle_vwq_exceptions() -> Generator[None]:
"""Show error messages and catch exceptions from ``VWS-Python``."""
try:
yield
except BadImageError:
error_message = (
"Error: The given image is corrupted or the format is not "
"supported."
)
except InactiveProjectError:
error_message = (
"Error: The project associated with the given keys is inactive."
)
except AuthenticationFailureError:
error_message = "The given secret key was incorrect."
except RequestTimeTooSkewedError:
error_message = (
"Error: Vuforia reported that the time given with this request "
"was outside the expected range. "
"This may be because the system clock is out of sync."
)
except RequestEntityTooLargeError:
error_message = "Error: The given image is too large."
except (CloudRecoError, RequestEntityTooLargeError, ServerError) as exc:
error_message = _get_cloud_reco_error_message(exc=exc)
else:
return

Expand Down
Loading
Loading