33import asyncio
44import base64
55import json
6+ import time
67from http import HTTPMethod , HTTPStatus
78from typing import Self
89
1112from vws ._async_vws_request import async_target_api_request
1213from vws ._image_utils import ImageType as _ImageType
1314from vws ._image_utils import get_image_data as _get_image_data
15+ from vws ._reco_counts import (
16+ reco_counts_report_body ,
17+ reco_counts_report_path ,
18+ report_from_download_response ,
19+ )
1420from vws .exceptions .base_exceptions import VWSError
1521from vws .exceptions .custom_exceptions import (
22+ RecoCountsReportNotReadyError ,
23+ RecoCountsReportTimeoutError ,
1624 ServerError ,
1725 TargetProcessingTimeoutError ,
1826)
1927from vws .exceptions .vws_exceptions import TooManyRequestsError
2028from vws .reports import (
2129 DatabaseSummaryReport ,
30+ RecoCountsReport ,
31+ RecoCountsReportRequest ,
2232 TargetStatusAndRecord ,
2333 TargetStatuses ,
2434 TargetSummaryReport ,
@@ -37,6 +47,7 @@ def __init__(
3747 server_access_key : str ,
3848 server_secret_key : str ,
3949 base_vws_url : str = "https://vws.vuforia.com" ,
50+ database_id : str | None = None ,
4051 request_timeout_seconds : float | tuple [float , float ] = 30.0 ,
4152 transport : AsyncTransport | None = None ,
4253 ) -> None :
@@ -45,6 +56,10 @@ def __init__(
4556 server_access_key: A VWS server access key.
4657 server_secret_key: A VWS server secret key.
4758 base_vws_url: The base URL for the VWS API.
59+ database_id: The ID of the database which the
60+ given keys belong to. This is shown in the
61+ target manager. It is needed only by
62+ :meth:`request_database_reco_counts_report`.
4863 request_timeout_seconds: The timeout for each
4964 HTTP request. This can be a float to set both
5065 the connect and read timeouts, or a
@@ -56,6 +71,7 @@ def __init__(
5671 self ._server_access_key = server_access_key
5772 self ._server_secret_key = server_secret_key
5873 self ._base_vws_url = base_vws_url
74+ self ._database_id = database_id
5975 self ._request_timeout_seconds = request_timeout_seconds
6076 self ._transport = (
6177 transport if transport is not None else AsyncHTTPXTransport ()
@@ -439,6 +455,133 @@ async def get_database_summary_report(
439455 response_dict = response_data ,
440456 )
441457
458+ async def request_database_reco_counts_report (
459+ self ,
460+ * ,
461+ month : str ,
462+ ) -> RecoCountsReportRequest :
463+ """Request a per-target recognition count report for the database.
464+
465+ Vuforia generates the report in the background, so the report is not
466+ available to download immediately. Use
467+ :meth:`wait_for_reco_counts_report` to wait for it.
468+
469+ Args:
470+ month: The month to get recognition counts for, in ``YYYY-mm``
471+ form. Vuforia accepts only the current month and the previous
472+ month.
473+
474+ Returns:
475+ The URL to download the report from, and the transaction ID of
476+ the request.
477+
478+ Raises:
479+ ~vws.exceptions.custom_exceptions.DatabaseIdNotSetError: No
480+ ``database_id`` was given to the client.
481+ ~vws.exceptions.vws_exceptions.AuthenticationFailureError: The
482+ secret key is not correct, or the client's ``database_id`` is
483+ not the ID of the database which the client's keys belong to.
484+ ~vws.exceptions.vws_exceptions.FailError: There was an error with
485+ the request. For example, the given month is not the current
486+ month or the previous month.
487+ ~vws.exceptions.vws_exceptions.RequestTimeTooSkewedError: There is
488+ an error with the time sent to Vuforia.
489+ ~vws.exceptions.custom_exceptions.ServerError: There is an error
490+ with Vuforia's servers.
491+ ~vws.exceptions.vws_exceptions.TooManyRequestsError: Vuforia is
492+ rate limiting access.
493+ """
494+ response = await self .make_request (
495+ method = HTTPMethod .POST ,
496+ data = reco_counts_report_body (month = month ),
497+ request_path = reco_counts_report_path (
498+ database_id = self ._database_id ,
499+ ),
500+ expected_result_code = "Success" ,
501+ content_type = "application/json" ,
502+ )
503+
504+ response_data = dict (json .loads (s = response .text ))
505+ return RecoCountsReportRequest .from_response_dict (
506+ response_dict = response_data ,
507+ )
508+
509+ async def download_reco_counts_report (
510+ self ,
511+ * ,
512+ presigned_url : str ,
513+ ) -> RecoCountsReport :
514+ """Download a requested reco counts report.
515+
516+ The report's URL is not part of the VWS API, so this request is not
517+ authorized with the client's keys.
518+
519+ Args:
520+ presigned_url: The URL of the report, as given by
521+ :meth:`request_database_reco_counts_report`.
522+
523+ Returns:
524+ The downloaded report.
525+
526+ Raises:
527+ ~vws.exceptions.custom_exceptions.RecoCountsReportNotReadyError:
528+ Vuforia has not finished generating the report.
529+ ~vws.exceptions.custom_exceptions.RecoCountsReportDownloadError:
530+ The report could not be downloaded. For example, the report's
531+ URL may have expired.
532+ """
533+ response = await self ._transport (
534+ method = HTTPMethod .GET ,
535+ url = presigned_url ,
536+ headers = {},
537+ data = b"" ,
538+ request_timeout = self ._request_timeout_seconds ,
539+ )
540+
541+ return report_from_download_response (response = response )
542+
543+ async def wait_for_reco_counts_report (
544+ self ,
545+ * ,
546+ presigned_url : str ,
547+ seconds_between_requests : float = 0.2 ,
548+ timeout_seconds : float = 60 * 5 ,
549+ ) -> RecoCountsReport :
550+ """Wait for a requested reco counts report to be generated, then
551+ download it.
552+
553+ Args:
554+ presigned_url: The URL of the report, as given by
555+ :meth:`request_database_reco_counts_report`.
556+ seconds_between_requests: The number of seconds to wait between
557+ requests made while polling the report's URL.
558+ timeout_seconds: The maximum number of seconds to wait for the
559+ report to be generated.
560+
561+ Returns:
562+ The downloaded report.
563+
564+ Raises:
565+ ~vws.exceptions.custom_exceptions.RecoCountsReportTimeoutError:
566+ The report was not generated within ``timeout_seconds``
567+ seconds.
568+ ~vws.exceptions.custom_exceptions.RecoCountsReportDownloadError:
569+ The report could not be downloaded. For example, the report's
570+ URL may have expired.
571+ """
572+ start_time = time .monotonic ()
573+ while True :
574+ try :
575+ return await self .download_reco_counts_report (
576+ presigned_url = presigned_url ,
577+ )
578+ except RecoCountsReportNotReadyError :
579+ elapsed_time = time .monotonic () - start_time
580+ if elapsed_time > timeout_seconds :
581+ raise RecoCountsReportTimeoutError from None
582+
583+ await asyncio .sleep (delay = seconds_between_requests )
584+
442585 async def delete_target (self , target_id : str ) -> None :
443586 """Delete a given target.
444587
0 commit comments