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
1 change: 1 addition & 0 deletions newsfragments/2441.change.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Add a ``vws get-database-reco-counts-report`` command, which requests a per-target recognition counts report for a month, waits for Vuforia to generate it, and writes the CSV to stdout or to a given path.
2 changes: 2 additions & 0 deletions spelling_private_dict.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
CSV
Winget
admin
api
Expand Down Expand Up @@ -33,6 +34,7 @@ reportMissingTypeStubs
reportUnknownArgumentType
reportUnknownMemberType
reportUnknownVariableType
stdout
svg
typeshed
ubuntu
Expand Down
2 changes: 2 additions & 0 deletions src/vws_cli/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
from vws_cli.commands import (
add_target,
delete_target,
get_database_reco_counts_report,
get_database_summary_report,
get_duplicate_targets,
get_target_record,
Expand Down Expand Up @@ -41,6 +42,7 @@ def vws_group() -> None:

vws_group.add_command(cmd=add_target)
vws_group.add_command(cmd=delete_target)
vws_group.add_command(cmd=get_database_reco_counts_report)
vws_group.add_command(cmd=get_database_summary_report)
vws_group.add_command(cmd=get_duplicate_targets)
vws_group.add_command(cmd=get_target_record)
Expand Down
4 changes: 4 additions & 0 deletions src/vws_cli/_error_handling.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

from beartype import beartype
from vws.exceptions.custom_exceptions import (
RecoCountsReportDownloadError,
RecoCountsReportTimeoutError,
ServerError,
TargetProcessingTimeoutError,
)
Expand Down Expand Up @@ -35,6 +37,8 @@ def get_error_message(exc: Exception) -> str:
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.",
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.",
RequestQuotaReachedError: "Error: The maximum number of API calls for this database has been reached.",
Expand Down
189 changes: 188 additions & 1 deletion src/vws_cli/commands.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,27 @@
"""``click`` commands the VWS CLI."""

import calendar
import contextlib
import dataclasses
import datetime
import io
import sys
from collections.abc import Generator
from pathlib import Path
from zoneinfo import ZoneInfo

import click
import yaml
from beartype import beartype
from vws import VWS
from vws.exceptions.base_exceptions import VWSError
from vws.exceptions.custom_exceptions import (
RecoCountsReportDownloadError,
RecoCountsReportTimeoutError,
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 All @@ -35,7 +41,7 @@
connection_timeout_seconds_option,
read_timeout_seconds_option,
)
from vws_cli.options.vws import base_vws_url_option
from vws_cli.options.vws import base_vws_url_option, database_id_option


@beartype
Expand All @@ -48,6 +54,7 @@ def _handle_vws_exceptions() -> Generator[None]:
yield
except (
VWSError,
RecoCountsReportDownloadError,
ServerError,
TargetProcessingTimeoutError,
) as exc:
Expand Down Expand Up @@ -488,3 +495,183 @@ def wait_for_target_processed(
err=True,
)
sys.exit(1)


_MONTH_FORMAT = "%Y-%m"

_REPORT_SECONDS_BETWEEN_REQUESTS_HELP = (
"The number of seconds to wait between requests made while polling for "
"the report. "
f"We wait {_SECONDS_BETWEEN_REQUESTS_DEFAULT} seconds by default, rather "
"than less, than that to decrease the number of calls made to the API, to "
"decrease the likelihood of hitting the request quota."
)

_REPORT_TIMEOUT_SECONDS_HELP = (
"The maximum number of seconds to wait for the report to be generated."
)


@beartype
def _default_month() -> str:
"""Return the current month, in the form which ``--month`` takes."""
now = datetime.datetime.now(tz=ZoneInfo(key="UTC"))
return now.strftime(format=_MONTH_FORMAT)


@beartype
def _validate_month(
ctx: click.Context,
param: click.Parameter,
value: str,
) -> datetime.date:
"""Turn a ``YYYY-mm`` string into the first day of that month."""
# These are given by ``click``, and we do not use them.
del ctx
del param
try:
parsed = datetime.datetime.strptime( # noqa: DTZ007
value,
_MONTH_FORMAT,
)
except ValueError as exc:
message = f'"{value}" is not a month in the YYYY-mm form.'
raise click.BadParameter(message=message) from exc
return parsed.date()


@click.command(name="get-database-reco-counts-report")
@click.option(
"--month",
type=str,
default=_default_month,
callback=_validate_month,
help=(
"The month to get recognition counts for, in the YYYY-mm form. "
"Vuforia accepts only the current month and the previous month."
),
show_default="the current month",
)
@click.option(
"--output",
"output_file_path",
type=click.Path(
dir_okay=False,
writable=True,
path_type=Path,
),
required=False,
help=(
"The path to write the CSV report to. By default, the report is "
"written to stdout."
),
)
@click.option(
"--no-wait",
"no_wait",
is_flag=True,
default=False,
help=(
"Do not wait for the report to be generated. Instead, show the URL "
"to download the report from once it has been generated."
),
)
@click.option(
"--seconds-between-requests",
type=click.FloatRange(min=0.05),
default=_SECONDS_BETWEEN_REQUESTS_DEFAULT,
help=_REPORT_SECONDS_BETWEEN_REQUESTS_HELP,
show_default=True,
)
@click.option(
"--timeout-seconds",
type=click.FloatRange(min=0.05),
default=300,
help=_REPORT_TIMEOUT_SECONDS_HELP,
show_default=True,
)
@server_access_key_option
@server_secret_key_option
@database_id_option
@base_vws_url_option
@connection_timeout_seconds_option
@read_timeout_seconds_option
@_handle_vws_exceptions()
@beartype
def get_database_reco_counts_report(
*,
server_access_key: str,
server_secret_key: str,
database_id: str,
month: datetime.date,
output_file_path: Path | None,
no_wait: bool,
seconds_between_requests: float,
timeout_seconds: float,
base_vws_url: str,
connection_timeout_seconds: float,
read_timeout_seconds: float,
) -> None:
"""Get a per-target recognition counts report for a database.

The report is a CSV with a ``target_id,reco_count`` header. Vuforia
generates the report in the background, so by default we wait for the
report to be generated before downloading it.

\b
See
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api.
"""
if no_wait and output_file_path is not None:
message = "--output cannot be used with --no-wait."
raise click.UsageError(message=message)

vws_client = VWS(
server_access_key=server_access_key,
server_secret_key=server_secret_key,
base_vws_url=base_vws_url,
database_id=database_id,
request_timeout_seconds=(
connection_timeout_seconds,
read_timeout_seconds,
),
)

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)

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)

if output_file_path is None:
click.echo(message=report.raw_csv, nl=False)
return

output_file_path.write_bytes(data=report.raw_csv)
18 changes: 18 additions & 0 deletions src/vws_cli/options/vws.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@
from beartype import beartype


@beartype
def database_id_option(
command: Callable[..., Any],
) -> Callable[..., Any]:
"""An option decorator for the Vuforia database ID."""
return click.option(
"--database-id",
type=str,
help=(
"The ID of the Vuforia database which the given server keys "
"belong to. This is shown in the Vuforia target manager."
),
required=True,
envvar="VUFORIA_DATABASE_ID",
show_envvar=True,
)(command)


@beartype
def base_vws_url_option(
command: Callable[..., Any],
Expand Down
20 changes: 11 additions & 9 deletions tests/test_help/test_vws_command_help____.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,14 @@ Options:
-h, --help Show this message and exit.

Commands:
add-target Add a target.
delete-target Delete a target.
get-database-summary-report Get a database summary report.
get-duplicate-targets Get a list of potential duplicate targets.
get-target-record Get a target record.
get-target-summary-report Get a target summary report.
list-targets List targets.
update-target Update a target.
wait-for-target-processed Wait for a target to be "processed".
add-target Add a target.
delete-target Delete a target.
get-database-reco-counts-report
Get a per-target recognition counts...
get-database-summary-report Get a database summary report.
get-duplicate-targets Get a list of potential duplicate targets.
get-target-record Get a target record.
get-target-summary-report Get a target summary report.
list-targets List targets.
update-target Update a target.
wait-for-target-processed Wait for a target to be "processed".
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
Usage: vws get-database-reco-counts-report [OPTIONS]

Get a per-target recognition counts report for a database.

The report is a CSV with a ``target_id,reco_count`` header. Vuforia generates
the report in the background, so by default we wait for the report to be
generated before downloading it.

See
https://developer.vuforia.com/library/web-api/cloud-targets-web-services-api.

Options:
--month TEXT The month to get recognition counts for, in
the YYYY-mm form. Vuforia accepts only the
current month and the previous month.
[default: (the current month)]
--output FILE The path to write the CSV report to. By
default, the report is written to stdout.
--no-wait Do not wait for the report to be generated.
Instead, show the URL to download the report
from once it has been generated.
--seconds-between-requests FLOAT RANGE
The number of seconds to wait between requests
made while polling for the report. We wait 0.2
seconds by default, rather than less, than
that to decrease the number of calls made to
the API, to decrease the likelihood of hitting
the request quota. [default: 0.2; x>=0.05]
--timeout-seconds FLOAT RANGE The maximum number of seconds to wait for the
report to be generated. [default: 300;
x>=0.05]
--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]
--database-id TEXT The ID of the Vuforia database which the given
server keys belong to. This is shown in the
Vuforia target manager. [env var:
VUFORIA_DATABASE_ID; 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]
-h, --help Show this message and exit.
Loading