diff --git a/.fern/metadata.json b/.fern/metadata.json index f1041dfc..b3ec1e42 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -14,5 +14,5 @@ "use_typeddict_requests_for_file_upload": true, "exclude_types_from_init_exports": true }, - "sdkVersion": "44.1.0.20260520" + "sdkVersion": "44.2.0.20260520" } \ No newline at end of file diff --git a/.fernignore b/.fernignore index 246906a5..2042ff79 100644 --- a/.fernignore +++ b/.fernignore @@ -9,6 +9,7 @@ examples legacy src/square/core/api_error.py src/square/utils/webhooks_helper.py +src/square/utils/reporting_helper.py tests/integration README.md .fern/replay.lock diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62bfdf61..0327a3e7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,7 @@ jobs: runs-on: ubuntu-latest env: TEST_SQUARE_TOKEN: ${{ secrets.TEST_SQUARE_TOKEN }} + TEST_SQUARE_REPORTING: ${{ secrets.TEST_SQUARE_REPORTING }} steps: - name: Checkout repo uses: actions/checkout@v3 diff --git a/README.md b/README.md index aa122514..b9111c2f 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,71 @@ is_valid = verify_signature( ) ``` +## Reporting API + +The [Reporting API](https://developer.squareup.com/docs/reporting-api/overview) lets you query +aggregated reporting data. Call `reporting.get_metadata` first to discover the available cubes, +measures, and dimensions, then run a query with `reporting.load`. + +```python +from square import Square + +client = Square(token="YOUR_TOKEN") + +# Discover what you can query. +metadata = client.reporting.get_metadata() + +# Run a query against the discovered schema. +response = client.reporting.load(query={"measures": ["Orders.count"]}) +``` + +`load` is asynchronous: while a query is still being computed, the API returns an HTTP `200` whose +body is `{"error": "Continue wait"}` instead of results, and the client is expected to re-send the +identical request — with backoff — until the results are ready. The `load_and_wait` helper owns +that polling loop for you and returns the resolved results (never the `"Continue wait"` sentinel): + +```python +from square import Square +from square.utils.reporting_helper import load_and_wait + +client = Square(token="YOUR_TOKEN") + +response = load_and_wait(client, query={"measures": ["Orders.count"]}) + +print(response.results) +``` + +By default it polls up to 20 times with exponential backoff (2s → 20s). Tune the behavior — and +pass a `threading.Event` to cancel — via the keyword arguments: + +```python +import threading + +cancel_event = threading.Event() + +response = load_and_wait( + client, + query={"measures": ["Orders.count"]}, + max_attempts=10, # default 20 + initial_delay_s=1.0, # default 2.0 + max_delay_s=20.0, # default 20.0 + backoff_factor=2.0, # default 2.0 + cancel_event=cancel_event, +) +``` + +For the [async client](#async-client), use `load_and_wait_async` (cancel it the idiomatic asyncio +way — e.g. `asyncio.wait_for` or `Task.cancel`): + +```python +from square import AsyncSquare +from square.utils.reporting_helper import load_and_wait_async + +client = AsyncSquare(token="YOUR_TOKEN") + +response = await load_and_wait_async(client, query={"measures": ["Orders.count"]}) +``` + ## Advanced ### Retries diff --git a/poetry.lock b/poetry.lock index dfc85896..1f9536dc 100644 --- a/poetry.lock +++ b/poetry.lock @@ -38,13 +38,13 @@ trio = ["trio (>=0.26.1)"] [[package]] name = "certifi" -version = "2026.4.22" +version = "2026.5.20" description = "Python package for providing Mozilla's CA Bundle." optional = false python-versions = ">=3.7" files = [ - {file = "certifi-2026.4.22-py3-none-any.whl", hash = "sha256:3cb2210c8f88ba2318d29b0388d1023c8492ff72ecdde4ebdaddbb13a31b1c4a"}, - {file = "certifi-2026.4.22.tar.gz", hash = "sha256:8d455352a37b71bf76a79caa83a3d6c25afee4a385d632127b6afb3963f1c580"}, + {file = "certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"}, + {file = "certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d"}, ] [[package]] diff --git a/pyproject.toml b/pyproject.toml index 98399848..37b24527 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "squareup" -version = "44.1.0.20260520" +version = "44.2.0.20260520" description = "" readme = "README.md" authors = [] diff --git a/reference.md b/reference.md index d35be41d..4857a1df 100644 --- a/reference.md +++ b/reference.md @@ -18511,6 +18511,151 @@ information. + + + + +## Reporting +
client.reporting.get_metadata() -> AsyncHttpResponse[MetadataResponse] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from square import Square + +client = Square( + token="YOUR_TOKEN", +) +client.reporting.get_metadata() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.reporting.load(...) -> AsyncHttpResponse[LoadResponse] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from square import Square + +client = Square( + token="YOUR_TOKEN", +) +client.reporting.load() + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**query_type:** `typing.Optional[str]` + +
+
+ +
+
+ +**cache:** `typing.Optional[CacheMode]` + +
+
+ +
+
+ +**query:** `typing.Optional[QueryParams]` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
diff --git a/src/square/__init__.py b/src/square/__init__.py index 63ebc92b..fb397371 100644 --- a/src/square/__init__.py +++ b/src/square/__init__.py @@ -32,6 +32,7 @@ payments, payouts, refunds, + reporting, sites, snippets, subscriptions, @@ -74,6 +75,7 @@ "payments": ".payments", "payouts": ".payouts", "refunds": ".refunds", + "reporting": ".reporting", "sites": ".sites", "snippets": ".snippets", "subscriptions": ".subscriptions", @@ -137,6 +139,7 @@ def __dir__(): "payments", "payouts", "refunds", + "reporting", "sites", "snippets", "subscriptions", diff --git a/src/square/client.py b/src/square/client.py index 4a0186d4..bbdcc76f 100644 --- a/src/square/client.py +++ b/src/square/client.py @@ -35,6 +35,7 @@ from .payments.client import AsyncPaymentsClient, PaymentsClient from .payouts.client import AsyncPayoutsClient, PayoutsClient from .refunds.client import AsyncRefundsClient, RefundsClient + from .reporting.client import AsyncReportingClient, ReportingClient from .sites.client import AsyncSitesClient, SitesClient from .snippets.client import AsyncSnippetsClient, SnippetsClient from .subscriptions.client import AsyncSubscriptionsClient, SubscriptionsClient @@ -148,6 +149,7 @@ def __init__( self._terminal: typing.Optional[TerminalClient] = None self._transfer_orders: typing.Optional[TransferOrdersClient] = None self._vendors: typing.Optional[VendorsClient] = None + self._reporting: typing.Optional[ReportingClient] = None self._cash_drawers: typing.Optional[CashDrawersClient] = None self._webhooks: typing.Optional[WebhooksClient] = None @@ -415,6 +417,14 @@ def vendors(self): self._vendors = VendorsClient(client_wrapper=self._client_wrapper) return self._vendors + @property + def reporting(self): + if self._reporting is None: + from .reporting.client import ReportingClient # noqa: E402 + + self._reporting = ReportingClient(client_wrapper=self._client_wrapper) + return self._reporting + @property def cash_drawers(self): if self._cash_drawers is None: @@ -533,6 +543,7 @@ def __init__( self._terminal: typing.Optional[AsyncTerminalClient] = None self._transfer_orders: typing.Optional[AsyncTransferOrdersClient] = None self._vendors: typing.Optional[AsyncVendorsClient] = None + self._reporting: typing.Optional[AsyncReportingClient] = None self._cash_drawers: typing.Optional[AsyncCashDrawersClient] = None self._webhooks: typing.Optional[AsyncWebhooksClient] = None @@ -800,6 +811,14 @@ def vendors(self): self._vendors = AsyncVendorsClient(client_wrapper=self._client_wrapper) return self._vendors + @property + def reporting(self): + if self._reporting is None: + from .reporting.client import AsyncReportingClient # noqa: E402 + + self._reporting = AsyncReportingClient(client_wrapper=self._client_wrapper) + return self._reporting + @property def cash_drawers(self): if self._cash_drawers is None: diff --git a/src/square/core/client_wrapper.py b/src/square/core/client_wrapper.py index b4f6b9f0..a584dadd 100644 --- a/src/square/core/client_wrapper.py +++ b/src/square/core/client_wrapper.py @@ -26,12 +26,12 @@ def get_headers(self) -> typing.Dict[str, str]: import platform headers: typing.Dict[str, str] = { - "User-Agent": "squareup/44.1.0.20260520", + "User-Agent": "squareup/44.2.0.20260520", "X-Fern-Language": "Python", "X-Fern-Runtime": f"python/{platform.python_version()}", "X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}", "X-Fern-SDK-Name": "squareup", - "X-Fern-SDK-Version": "44.1.0.20260520", + "X-Fern-SDK-Version": "44.2.0.20260520", **(self.get_custom_headers() or {}), } token = self._get_token() diff --git a/src/square/reporting/__init__.py b/src/square/reporting/__init__.py new file mode 100644 index 00000000..5cde0202 --- /dev/null +++ b/src/square/reporting/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/square/reporting/client.py b/src/square/reporting/client.py new file mode 100644 index 00000000..65567e6b --- /dev/null +++ b/src/square/reporting/client.py @@ -0,0 +1,196 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from ..requests.query import QueryParams +from ..types.cache_mode import CacheMode +from ..types.load_response import LoadResponse +from ..types.metadata_response import MetadataResponse +from .raw_client import AsyncRawReportingClient, RawReportingClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class ReportingClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawReportingClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawReportingClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawReportingClient + """ + return self._raw_client + + def get_metadata(self, *, request_options: typing.Optional[RequestOptions] = None) -> MetadataResponse: + """ + Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MetadataResponse + successful operation + + Examples + -------- + from square import Square + + client = Square( + token="YOUR_TOKEN", + ) + client.reporting.get_metadata() + """ + _response = self._raw_client.get_metadata(request_options=request_options) + return _response.data + + def load( + self, + *, + query_type: typing.Optional[str] = OMIT, + cache: typing.Optional[CacheMode] = OMIT, + query: typing.Optional[QueryParams] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> LoadResponse: + """ + Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready. + + Parameters + ---------- + query_type : typing.Optional[str] + + cache : typing.Optional[CacheMode] + + query : typing.Optional[QueryParams] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + LoadResponse + successful operation + + Examples + -------- + from square import Square + + client = Square( + token="YOUR_TOKEN", + ) + client.reporting.load() + """ + _response = self._raw_client.load( + query_type=query_type, cache=cache, query=query, request_options=request_options + ) + return _response.data + + +class AsyncReportingClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawReportingClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawReportingClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawReportingClient + """ + return self._raw_client + + async def get_metadata(self, *, request_options: typing.Optional[RequestOptions] = None) -> MetadataResponse: + """ + Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + MetadataResponse + successful operation + + Examples + -------- + import asyncio + + from square import AsyncSquare + + client = AsyncSquare( + token="YOUR_TOKEN", + ) + + + async def main() -> None: + await client.reporting.get_metadata() + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_metadata(request_options=request_options) + return _response.data + + async def load( + self, + *, + query_type: typing.Optional[str] = OMIT, + cache: typing.Optional[CacheMode] = OMIT, + query: typing.Optional[QueryParams] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> LoadResponse: + """ + Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready. + + Parameters + ---------- + query_type : typing.Optional[str] + + cache : typing.Optional[CacheMode] + + query : typing.Optional[QueryParams] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + LoadResponse + successful operation + + Examples + -------- + import asyncio + + from square import AsyncSquare + + client = AsyncSquare( + token="YOUR_TOKEN", + ) + + + async def main() -> None: + await client.reporting.load() + + + asyncio.run(main()) + """ + _response = await self._raw_client.load( + query_type=query_type, cache=cache, query=query, request_options=request_options + ) + return _response.data diff --git a/src/square/reporting/raw_client.py b/src/square/reporting/raw_client.py new file mode 100644 index 00000000..d1b7d3b5 --- /dev/null +++ b/src/square/reporting/raw_client.py @@ -0,0 +1,216 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.request_options import RequestOptions +from ..core.serialization import convert_and_respect_annotation_metadata +from ..core.unchecked_base_model import construct_type +from ..requests.query import QueryParams +from ..types.cache_mode import CacheMode +from ..types.load_response import LoadResponse +from ..types.metadata_response import MetadataResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawReportingClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def get_metadata( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[MetadataResponse]: + """ + Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[MetadataResponse] + successful operation + """ + _response = self._client_wrapper.httpx_client.request( + "reporting/v1/meta", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MetadataResponse, + construct_type( + type_=MetadataResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + def load( + self, + *, + query_type: typing.Optional[str] = OMIT, + cache: typing.Optional[CacheMode] = OMIT, + query: typing.Optional[QueryParams] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[LoadResponse]: + """ + Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready. + + Parameters + ---------- + query_type : typing.Optional[str] + + cache : typing.Optional[CacheMode] + + query : typing.Optional[QueryParams] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[LoadResponse] + successful operation + """ + _response = self._client_wrapper.httpx_client.request( + "reporting/v1/load", + method="POST", + json={ + "queryType": query_type, + "cache": cache, + "query": convert_and_respect_annotation_metadata( + object_=query, annotation=QueryParams, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + LoadResponse, + construct_type( + type_=LoadResponse, # type: ignore + object_=_response.json(), + ), + ) + return HttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + +class AsyncRawReportingClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def get_metadata( + self, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[MetadataResponse]: + """ + Describes the data available to query: the cubes, views, measures, dimensions, and segments you can reference in a reporting query. Call this first to discover the schema, then pass the members you need to `load`. + + Parameters + ---------- + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[MetadataResponse] + successful operation + """ + _response = await self._client_wrapper.httpx_client.request( + "reporting/v1/meta", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + MetadataResponse, + construct_type( + type_=MetadataResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) + + async def load( + self, + *, + query_type: typing.Optional[str] = OMIT, + cache: typing.Optional[CacheMode] = OMIT, + query: typing.Optional[QueryParams] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[LoadResponse]: + """ + Runs a reporting query against the discovered schema and returns the aggregated results. Long-running queries may return a "Continue wait" response while processing — retry the same request until results are ready. + + Parameters + ---------- + query_type : typing.Optional[str] + + cache : typing.Optional[CacheMode] + + query : typing.Optional[QueryParams] + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[LoadResponse] + successful operation + """ + _response = await self._client_wrapper.httpx_client.request( + "reporting/v1/load", + method="POST", + json={ + "queryType": query_type, + "cache": cache, + "query": convert_and_respect_annotation_metadata( + object_=query, annotation=QueryParams, direction="write" + ), + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + LoadResponse, + construct_type( + type_=LoadResponse, # type: ignore + object_=_response.json(), + ), + ) + return AsyncHttpResponse(response=_response, data=_data) + _response_json = _response.json() + except JSONDecodeError: + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text) + raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json) diff --git a/src/square/requests/cube.py b/src/square/requests/cube.py new file mode 100644 index 00000000..8936da33 --- /dev/null +++ b/src/square/requests/cube.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.cube_type import CubeType +from .cube_join import CubeJoinParams +from .dimension import DimensionParams +from .folder import FolderParams +from .hierarchy import HierarchyParams +from .measure import MeasureParams +from .nested_folder import NestedFolderParams +from .segment import SegmentParams + + +class CubeParams(typing_extensions.TypedDict): + name: str + title: typing_extensions.NotRequired[str] + type: CubeType + meta: typing_extensions.NotRequired[typing.Dict[str, typing.Any]] + description: typing_extensions.NotRequired[str] + measures: typing.Sequence[MeasureParams] + dimensions: typing.Sequence[DimensionParams] + segments: typing.Sequence[SegmentParams] + joins: typing_extensions.NotRequired[typing.Sequence[CubeJoinParams]] + folders: typing_extensions.NotRequired[typing.Sequence[FolderParams]] + nested_folders: typing_extensions.NotRequired[ + typing_extensions.Annotated[typing.Sequence[NestedFolderParams], FieldMetadata(alias="nestedFolders")] + ] + hierarchies: typing_extensions.NotRequired[typing.Sequence[HierarchyParams]] diff --git a/src/square/requests/cube_join.py b/src/square/requests/cube_join.py new file mode 100644 index 00000000..d84a9587 --- /dev/null +++ b/src/square/requests/cube_join.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class CubeJoinParams(typing_extensions.TypedDict): + name: str + relationship: str diff --git a/src/square/requests/custom_numeric_format.py b/src/square/requests/custom_numeric_format.py new file mode 100644 index 00000000..15b8123f --- /dev/null +++ b/src/square/requests/custom_numeric_format.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class CustomNumericFormatParams(typing_extensions.TypedDict): + """ + Custom numeric format for numeric measures and dimensions + """ + + type: typing.Literal["custom-numeric"] + """ + Type of the format (must be 'custom-numeric') + """ + + value: str + """ + d3-format specifier string (e.g., '.2f', ',.0f', '$,.2f', '.0%', '.2s'). See https://d3js.org/d3-format + """ + + alias: typing_extensions.NotRequired[str] + """ + Name of the predefined format (e.g., 'percent_2', 'currency_1'). Present only when a named format was used. + """ diff --git a/src/square/requests/custom_time_format.py b/src/square/requests/custom_time_format.py new file mode 100644 index 00000000..bba2872f --- /dev/null +++ b/src/square/requests/custom_time_format.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class CustomTimeFormatParams(typing_extensions.TypedDict): + """ + Custom time format for time dimensions + """ + + type: typing.Literal["custom-time"] + """ + Type of the format (must be 'custom-time') + """ + + value: str + """ + POSIX strftime format string (IEEE Std 1003.1 / POSIX.1) with d3-time-format extensions (e.g., '%Y-%m-%d', '%d/%m/%Y %H:%M:%S'). See https://pubs.opengroup.org/onlinepubs/009695399/functions/strptime.html and https://d3js.org/d3-time-format + """ diff --git a/src/square/requests/dimension.py b/src/square/requests/dimension.py new file mode 100644 index 00000000..d9037136 --- /dev/null +++ b/src/square/requests/dimension.py @@ -0,0 +1,39 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.dimension_order import DimensionOrder +from .dimension_granularity import DimensionGranularityParams +from .format import FormatParams +from .format_description import FormatDescriptionParams + + +class DimensionParams(typing_extensions.TypedDict): + name: str + title: typing_extensions.NotRequired[str] + short_title: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="shortTitle")]] + description: typing_extensions.NotRequired[str] + type: str + alias_member: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="aliasMember")]] + """ + When dimension is defined in View, it keeps the original path: Cube.dimension + """ + + granularities: typing_extensions.NotRequired[typing.Sequence[DimensionGranularityParams]] + meta: typing_extensions.NotRequired[typing.Dict[str, typing.Any]] + format: typing_extensions.NotRequired[FormatParams] + format_description: typing_extensions.NotRequired[ + typing_extensions.Annotated[FormatDescriptionParams, FieldMetadata(alias="formatDescription")] + ] + currency: typing_extensions.NotRequired[str] + """ + ISO 4217 currency code in uppercase (3 characters, e.g. USD, EUR) + """ + + order: typing_extensions.NotRequired[DimensionOrder] + key: typing_extensions.NotRequired[str] + """ + Key reference for the dimension + """ diff --git a/src/square/requests/dimension_granularity.py b/src/square/requests/dimension_granularity.py new file mode 100644 index 00000000..f8c0b20a --- /dev/null +++ b/src/square/requests/dimension_granularity.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class DimensionGranularityParams(typing_extensions.TypedDict): + name: str + title: str + interval: typing_extensions.NotRequired[str] + sql: typing_extensions.NotRequired[str] + offset: typing_extensions.NotRequired[str] + origin: typing_extensions.NotRequired[str] diff --git a/src/square/requests/folder.py b/src/square/requests/folder.py new file mode 100644 index 00000000..3e6f4e75 --- /dev/null +++ b/src/square/requests/folder.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class FolderParams(typing_extensions.TypedDict): + name: str + members: typing.Sequence[str] diff --git a/src/square/requests/format.py b/src/square/requests/format.py new file mode 100644 index 00000000..972e38a0 --- /dev/null +++ b/src/square/requests/format.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..types.simple_format import SimpleFormat +from .custom_numeric_format import CustomNumericFormatParams +from .custom_time_format import CustomTimeFormatParams +from .link_format import LinkFormatParams + +FormatParams = typing.Union[SimpleFormat, LinkFormatParams, CustomTimeFormatParams, CustomNumericFormatParams] diff --git a/src/square/requests/format_description.py b/src/square/requests/format_description.py new file mode 100644 index 00000000..6a91180d --- /dev/null +++ b/src/square/requests/format_description.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class FormatDescriptionParams(typing_extensions.TypedDict): + """ + Resolved format description with the predefined name and d3-format specifier + """ + + name: str + """ + Predefined format name (e.g., 'percent_2', 'currency_1') or a base name like 'number', or 'custom' for ad-hoc specifiers + """ + + specifier: str + """ + d3-format specifier string (e.g., '.2f', ',.0f', '$,.2f'). See https://d3js.org/d3-format + """ + + currency: typing_extensions.NotRequired[str] + """ + ISO 4217 currency code in uppercase (e.g. USD, EUR). Present when a currency format is used. + """ diff --git a/src/square/requests/hierarchy.py b/src/square/requests/hierarchy.py new file mode 100644 index 00000000..4365b8de --- /dev/null +++ b/src/square/requests/hierarchy.py @@ -0,0 +1,17 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class HierarchyParams(typing_extensions.TypedDict): + name: str + alias_member: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="aliasMember")]] + """ + When hierarchy is defined in Cube, it keeps the original path: Cube.hierarchy + """ + + title: typing_extensions.NotRequired[str] + levels: typing.Sequence[str] diff --git a/src/square/requests/join_subquery.py b/src/square/requests/join_subquery.py new file mode 100644 index 00000000..8d476d98 --- /dev/null +++ b/src/square/requests/join_subquery.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class JoinSubqueryParams(typing_extensions.TypedDict): + sql: str + on: str + join_type: typing_extensions.Annotated[str, FieldMetadata(alias="joinType")] + alias: str diff --git a/src/square/requests/link_format.py b/src/square/requests/link_format.py new file mode 100644 index 00000000..61cc7f39 --- /dev/null +++ b/src/square/requests/link_format.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class LinkFormatParams(typing_extensions.TypedDict): + """ + Link format with label and type + """ + + label: str + """ + Label for the link + """ + + type: typing.Literal["link"] + """ + Type of the format (must be 'link') + """ diff --git a/src/square/requests/load_response.py b/src/square/requests/load_response.py new file mode 100644 index 00000000..96eba7ee --- /dev/null +++ b/src/square/requests/load_response.py @@ -0,0 +1,16 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .load_result import LoadResultParams + + +class LoadResponseParams(typing_extensions.TypedDict): + pivot_query: typing_extensions.NotRequired[ + typing_extensions.Annotated[typing.Dict[str, typing.Any], FieldMetadata(alias="pivotQuery")] + ] + slow_query: typing_extensions.NotRequired[typing_extensions.Annotated[bool, FieldMetadata(alias="slowQuery")]] + query_type: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="queryType")]] + results: typing.Sequence[LoadResultParams] diff --git a/src/square/requests/load_result.py b/src/square/requests/load_result.py new file mode 100644 index 00000000..eec1375a --- /dev/null +++ b/src/square/requests/load_result.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .load_result_annotation import LoadResultAnnotationParams +from .load_result_data import LoadResultDataParams + + +class LoadResultParams(typing_extensions.TypedDict): + data_source: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="dataSource")]] + annotation: LoadResultAnnotationParams + data: LoadResultDataParams + refresh_key_values: typing_extensions.NotRequired[ + typing_extensions.Annotated[ + typing.Sequence[typing.Dict[str, typing.Any]], FieldMetadata(alias="refreshKeyValues") + ] + ] + last_refresh_time: typing_extensions.NotRequired[ + typing_extensions.Annotated[str, FieldMetadata(alias="lastRefreshTime")] + ] diff --git a/src/square/requests/load_result_annotation.py b/src/square/requests/load_result_annotation.py new file mode 100644 index 00000000..5389dd86 --- /dev/null +++ b/src/square/requests/load_result_annotation.py @@ -0,0 +1,13 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class LoadResultAnnotationParams(typing_extensions.TypedDict): + measures: typing.Dict[str, typing.Any] + dimensions: typing.Dict[str, typing.Any] + segments: typing.Dict[str, typing.Any] + time_dimensions: typing_extensions.Annotated[typing.Dict[str, typing.Any], FieldMetadata(alias="timeDimensions")] diff --git a/src/square/requests/load_result_data.py b/src/square/requests/load_result_data.py new file mode 100644 index 00000000..98b84717 --- /dev/null +++ b/src/square/requests/load_result_data.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ..types.load_result_data_row import LoadResultDataRow +from .load_result_data_columnar import LoadResultDataColumnarParams +from .load_result_data_compact import LoadResultDataCompactParams + +LoadResultDataParams = typing.Union[LoadResultDataRow, LoadResultDataCompactParams, LoadResultDataColumnarParams] diff --git a/src/square/requests/load_result_data_columnar.py b/src/square/requests/load_result_data_columnar.py new file mode 100644 index 00000000..f5507f95 --- /dev/null +++ b/src/square/requests/load_result_data_columnar.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class LoadResultDataColumnarParams(typing_extensions.TypedDict): + """ + Columnar data format - members list paired with one primitive array per column. Returned when `responseFormat=columnar` is requested. + """ + + members: typing.Sequence[str] + """ + Ordered list of member names. Element `i` of `columns` holds the values for `members[i]` across all rows. + """ + + columns: typing.Sequence[typing.Sequence[typing.Any]] + """ + One array per member, in the same order as `members`. Each inner array contains the primitive value of that member for every row (null, boolean, number, string). + """ diff --git a/src/square/requests/load_result_data_compact.py b/src/square/requests/load_result_data_compact.py new file mode 100644 index 00000000..1eec025e --- /dev/null +++ b/src/square/requests/load_result_data_compact.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class LoadResultDataCompactParams(typing_extensions.TypedDict): + """ + Compact data format - a single object with the members list and a dataset of primitive arrays. Returned when `responseFormat=compact` is requested. + """ + + members: typing.Sequence[str] + """ + Ordered list of member names that correspond to each cell position in `dataset` rows. + """ + + dataset: typing.Sequence[typing.Sequence[typing.Any]] + """ + Array of rows, where each row is an array of primitive values (null, boolean, number, string) aligned with `members`. + """ diff --git a/src/square/requests/measure.py b/src/square/requests/measure.py new file mode 100644 index 00000000..e1fb6c1f --- /dev/null +++ b/src/square/requests/measure.py @@ -0,0 +1,31 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .format import FormatParams +from .format_description import FormatDescriptionParams + + +class MeasureParams(typing_extensions.TypedDict): + name: str + title: typing_extensions.NotRequired[str] + short_title: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="shortTitle")]] + description: typing_extensions.NotRequired[str] + type: str + agg_type: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="aggType")]] + meta: typing_extensions.NotRequired[typing.Dict[str, typing.Any]] + format: typing_extensions.NotRequired[FormatParams] + format_description: typing_extensions.NotRequired[ + typing_extensions.Annotated[FormatDescriptionParams, FieldMetadata(alias="formatDescription")] + ] + currency: typing_extensions.NotRequired[str] + """ + ISO 4217 currency code in uppercase (3 characters, e.g. USD, EUR) + """ + + alias_member: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="aliasMember")]] + """ + When measure is defined in View, it keeps the original path: Cube.measure + """ diff --git a/src/square/requests/metadata_response.py b/src/square/requests/metadata_response.py new file mode 100644 index 00000000..e72e9595 --- /dev/null +++ b/src/square/requests/metadata_response.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from .cube import CubeParams + + +class MetadataResponseParams(typing_extensions.TypedDict): + cubes: typing_extensions.NotRequired[typing.Sequence[CubeParams]] + compiler_id: typing_extensions.NotRequired[typing_extensions.Annotated[str, FieldMetadata(alias="compilerId")]] diff --git a/src/square/requests/nested_folder.py b/src/square/requests/nested_folder.py new file mode 100644 index 00000000..9d269c07 --- /dev/null +++ b/src/square/requests/nested_folder.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class NestedFolderParams(typing_extensions.TypedDict): + name: str + members: typing.Sequence[str] diff --git a/src/square/requests/query.py b/src/square/requests/query.py new file mode 100644 index 00000000..59219296 --- /dev/null +++ b/src/square/requests/query.py @@ -0,0 +1,35 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata +from ..types.join_hint import JoinHint +from ..types.response_format import ResponseFormat +from .join_subquery import JoinSubqueryParams +from .query_filter import QueryFilterParams +from .time_dimension import TimeDimensionParams + + +class QueryParams(typing_extensions.TypedDict): + measures: typing_extensions.NotRequired[typing.Sequence[str]] + dimensions: typing_extensions.NotRequired[typing.Sequence[str]] + segments: typing_extensions.NotRequired[typing.Sequence[str]] + time_dimensions: typing_extensions.NotRequired[ + typing_extensions.Annotated[typing.Sequence[TimeDimensionParams], FieldMetadata(alias="timeDimensions")] + ] + order: typing_extensions.NotRequired[typing.Sequence[typing.Sequence[str]]] + limit: typing_extensions.NotRequired[int] + offset: typing_extensions.NotRequired[int] + filters: typing_extensions.NotRequired[typing.Sequence[QueryFilterParams]] + ungrouped: typing_extensions.NotRequired[bool] + subquery_joins: typing_extensions.NotRequired[ + typing_extensions.Annotated[typing.Sequence[JoinSubqueryParams], FieldMetadata(alias="subqueryJoins")] + ] + join_hints: typing_extensions.NotRequired[ + typing_extensions.Annotated[typing.Sequence[JoinHint], FieldMetadata(alias="joinHints")] + ] + timezone: typing_extensions.NotRequired[str] + response_format: typing_extensions.NotRequired[ + typing_extensions.Annotated[ResponseFormat, FieldMetadata(alias="responseFormat")] + ] diff --git a/src/square/requests/query_filter.py b/src/square/requests/query_filter.py new file mode 100644 index 00000000..39f89f0f --- /dev/null +++ b/src/square/requests/query_filter.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .query_filter_and import QueryFilterAndParams +from .query_filter_condition import QueryFilterConditionParams +from .query_filter_or import QueryFilterOrParams + +QueryFilterParams = typing.Union[QueryFilterConditionParams, QueryFilterOrParams, QueryFilterAndParams] diff --git a/src/square/requests/query_filter_and.py b/src/square/requests/query_filter_and.py new file mode 100644 index 00000000..0fec90ae --- /dev/null +++ b/src/square/requests/query_filter_and.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class QueryFilterAndParams(typing_extensions.TypedDict): + and_: typing_extensions.NotRequired[ + typing_extensions.Annotated[typing.Sequence[typing.Dict[str, typing.Any]], FieldMetadata(alias="and")] + ] diff --git a/src/square/requests/query_filter_condition.py b/src/square/requests/query_filter_condition.py new file mode 100644 index 00000000..45f27907 --- /dev/null +++ b/src/square/requests/query_filter_condition.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions + + +class QueryFilterConditionParams(typing_extensions.TypedDict): + member: typing_extensions.NotRequired[str] + operator: typing_extensions.NotRequired[str] + values: typing_extensions.NotRequired[typing.Sequence[str]] diff --git a/src/square/requests/query_filter_or.py b/src/square/requests/query_filter_or.py new file mode 100644 index 00000000..0cb5bfdc --- /dev/null +++ b/src/square/requests/query_filter_or.py @@ -0,0 +1,12 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class QueryFilterOrParams(typing_extensions.TypedDict): + or_: typing_extensions.NotRequired[ + typing_extensions.Annotated[typing.Sequence[typing.Dict[str, typing.Any]], FieldMetadata(alias="or")] + ] diff --git a/src/square/requests/reporting_error.py b/src/square/requests/reporting_error.py new file mode 100644 index 00000000..bc987547 --- /dev/null +++ b/src/square/requests/reporting_error.py @@ -0,0 +1,11 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing_extensions + + +class ReportingErrorParams(typing_extensions.TypedDict): + """ + Error envelope returned by the Reporting API. Note: a 200 response whose body is `{ "error": "Continue wait" }` is not a failure — it signals that a long-running query is still processing and the request should be retried. + """ + + error: str diff --git a/src/square/requests/segment.py b/src/square/requests/segment.py new file mode 100644 index 00000000..a2f143f4 --- /dev/null +++ b/src/square/requests/segment.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class SegmentParams(typing_extensions.TypedDict): + name: str + title: str + description: typing_extensions.NotRequired[str] + short_title: typing_extensions.Annotated[str, FieldMetadata(alias="shortTitle")] + meta: typing_extensions.NotRequired[typing.Dict[str, typing.Any]] diff --git a/src/square/requests/time_dimension.py b/src/square/requests/time_dimension.py new file mode 100644 index 00000000..71f03167 --- /dev/null +++ b/src/square/requests/time_dimension.py @@ -0,0 +1,14 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import typing_extensions +from ..core.serialization import FieldMetadata + + +class TimeDimensionParams(typing_extensions.TypedDict): + dimension: str + granularity: typing_extensions.NotRequired[str] + date_range: typing_extensions.NotRequired[ + typing_extensions.Annotated[typing.Dict[str, typing.Any], FieldMetadata(alias="dateRange")] + ] diff --git a/src/square/types/cache_mode.py b/src/square/types/cache_mode.py new file mode 100644 index 00000000..76c99ce0 --- /dev/null +++ b/src/square/types/cache_mode.py @@ -0,0 +1,7 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +CacheMode = typing.Union[ + typing.Literal["stale-if-slow", "stale-while-revalidate", "must-revalidate", "no-cache"], typing.Any +] diff --git a/src/square/types/cube.py b/src/square/types/cube.py new file mode 100644 index 00000000..5b9c9f2b --- /dev/null +++ b/src/square/types/cube.py @@ -0,0 +1,45 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .cube_join import CubeJoin +from .cube_type import CubeType +from .dimension import Dimension +from .folder import Folder +from .hierarchy import Hierarchy +from .measure import Measure +from .nested_folder import NestedFolder +from .segment import Segment + + +class Cube(UncheckedBaseModel): + name: str + title: typing.Optional[str] = None + type: CubeType + meta: typing.Optional[typing.Dict[str, typing.Any]] = None + description: typing.Optional[str] = None + measures: typing.List[Measure] + dimensions: typing.List[Dimension] + segments: typing.List[Segment] + joins: typing.Optional[typing.List[CubeJoin]] = None + folders: typing.Optional[typing.List[Folder]] = None + nested_folders: typing_extensions.Annotated[ + typing.Optional[typing.List[NestedFolder]], + FieldMetadata(alias="nestedFolders"), + pydantic.Field(alias="nestedFolders"), + ] = None + hierarchies: typing.Optional[typing.List[Hierarchy]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/cube_join.py b/src/square/types/cube_join.py new file mode 100644 index 00000000..48196606 --- /dev/null +++ b/src/square/types/cube_join.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class CubeJoin(UncheckedBaseModel): + name: str + relationship: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/cube_type.py b/src/square/types/cube_type.py new file mode 100644 index 00000000..e771d193 --- /dev/null +++ b/src/square/types/cube_type.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +CubeType = typing.Union[typing.Literal["cube", "view"], typing.Any] diff --git a/src/square/types/custom_numeric_format.py b/src/square/types/custom_numeric_format.py new file mode 100644 index 00000000..e3d7a269 --- /dev/null +++ b/src/square/types/custom_numeric_format.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class CustomNumericFormat(UncheckedBaseModel): + """ + Custom numeric format for numeric measures and dimensions + """ + + type: typing.Literal["custom-numeric"] = pydantic.Field(default="custom-numeric") + """ + Type of the format (must be 'custom-numeric') + """ + + value: str = pydantic.Field() + """ + d3-format specifier string (e.g., '.2f', ',.0f', '$,.2f', '.0%', '.2s'). See https://d3js.org/d3-format + """ + + alias: typing.Optional[str] = pydantic.Field(default=None) + """ + Name of the predefined format (e.g., 'percent_2', 'currency_1'). Present only when a named format was used. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/custom_time_format.py b/src/square/types/custom_time_format.py new file mode 100644 index 00000000..409da8f7 --- /dev/null +++ b/src/square/types/custom_time_format.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class CustomTimeFormat(UncheckedBaseModel): + """ + Custom time format for time dimensions + """ + + type: typing.Literal["custom-time"] = pydantic.Field(default="custom-time") + """ + Type of the format (must be 'custom-time') + """ + + value: str = pydantic.Field() + """ + POSIX strftime format string (IEEE Std 1003.1 / POSIX.1) with d3-time-format extensions (e.g., '%Y-%m-%d', '%d/%m/%Y %H:%M:%S'). See https://pubs.opengroup.org/onlinepubs/009695399/functions/strptime.html and https://d3js.org/d3-time-format + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/dimension.py b/src/square/types/dimension.py new file mode 100644 index 00000000..a4a6e999 --- /dev/null +++ b/src/square/types/dimension.py @@ -0,0 +1,58 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .dimension_granularity import DimensionGranularity +from .dimension_order import DimensionOrder +from .format import Format +from .format_description import FormatDescription + + +class Dimension(UncheckedBaseModel): + name: str + title: typing.Optional[str] = None + short_title: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="shortTitle"), pydantic.Field(alias="shortTitle") + ] = None + description: typing.Optional[str] = None + type: str + alias_member: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="aliasMember"), + pydantic.Field( + alias="aliasMember", + description="When dimension is defined in View, it keeps the original path: Cube.dimension", + ), + ] = None + granularities: typing.Optional[typing.List[DimensionGranularity]] = None + meta: typing.Optional[typing.Dict[str, typing.Any]] = None + format: typing.Optional[Format] = None + format_description: typing_extensions.Annotated[ + typing.Optional[FormatDescription], + FieldMetadata(alias="formatDescription"), + pydantic.Field(alias="formatDescription"), + ] = None + currency: typing.Optional[str] = pydantic.Field(default=None) + """ + ISO 4217 currency code in uppercase (3 characters, e.g. USD, EUR) + """ + + order: typing.Optional[DimensionOrder] = None + key: typing.Optional[str] = pydantic.Field(default=None) + """ + Key reference for the dimension + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/dimension_granularity.py b/src/square/types/dimension_granularity.py new file mode 100644 index 00000000..236d9a82 --- /dev/null +++ b/src/square/types/dimension_granularity.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class DimensionGranularity(UncheckedBaseModel): + name: str + title: str + interval: typing.Optional[str] = None + sql: typing.Optional[str] = None + offset: typing.Optional[str] = None + origin: typing.Optional[str] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/dimension_order.py b/src/square/types/dimension_order.py new file mode 100644 index 00000000..4062fa7a --- /dev/null +++ b/src/square/types/dimension_order.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +DimensionOrder = typing.Union[typing.Literal["asc", "desc"], typing.Any] diff --git a/src/square/types/folder.py b/src/square/types/folder.py new file mode 100644 index 00000000..01d458a5 --- /dev/null +++ b/src/square/types/folder.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class Folder(UncheckedBaseModel): + name: str + members: typing.List[str] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/format.py b/src/square/types/format.py new file mode 100644 index 00000000..7532e4b8 --- /dev/null +++ b/src/square/types/format.py @@ -0,0 +1,10 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .custom_numeric_format import CustomNumericFormat +from .custom_time_format import CustomTimeFormat +from .link_format import LinkFormat +from .simple_format import SimpleFormat + +Format = typing.Union[SimpleFormat, LinkFormat, CustomTimeFormat, CustomNumericFormat] diff --git a/src/square/types/format_description.py b/src/square/types/format_description.py new file mode 100644 index 00000000..17fc5f1a --- /dev/null +++ b/src/square/types/format_description.py @@ -0,0 +1,37 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class FormatDescription(UncheckedBaseModel): + """ + Resolved format description with the predefined name and d3-format specifier + """ + + name: str = pydantic.Field() + """ + Predefined format name (e.g., 'percent_2', 'currency_1') or a base name like 'number', or 'custom' for ad-hoc specifiers + """ + + specifier: str = pydantic.Field() + """ + d3-format specifier string (e.g., '.2f', ',.0f', '$,.2f'). See https://d3js.org/d3-format + """ + + currency: typing.Optional[str] = pydantic.Field(default=None) + """ + ISO 4217 currency code in uppercase (e.g. USD, EUR). Present when a currency format is used. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/hierarchy.py b/src/square/types/hierarchy.py new file mode 100644 index 00000000..ea084b6e --- /dev/null +++ b/src/square/types/hierarchy.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class Hierarchy(UncheckedBaseModel): + name: str + alias_member: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="aliasMember"), + pydantic.Field( + alias="aliasMember", + description="When hierarchy is defined in Cube, it keeps the original path: Cube.hierarchy", + ), + ] = None + title: typing.Optional[str] = None + levels: typing.List[str] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/join_hint.py b/src/square/types/join_hint.py new file mode 100644 index 00000000..b9692d7d --- /dev/null +++ b/src/square/types/join_hint.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +JoinHint = typing.List[str] diff --git a/src/square/types/join_subquery.py b/src/square/types/join_subquery.py new file mode 100644 index 00000000..777cb11c --- /dev/null +++ b/src/square/types/join_subquery.py @@ -0,0 +1,25 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class JoinSubquery(UncheckedBaseModel): + sql: str + on: str + join_type: typing_extensions.Annotated[str, FieldMetadata(alias="joinType"), pydantic.Field(alias="joinType")] + alias: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/link_format.py b/src/square/types/link_format.py new file mode 100644 index 00000000..423bc8a8 --- /dev/null +++ b/src/square/types/link_format.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class LinkFormat(UncheckedBaseModel): + """ + Link format with label and type + """ + + label: str = pydantic.Field() + """ + Label for the link + """ + + type: typing.Literal["link"] = pydantic.Field(default="link") + """ + Type of the format (must be 'link') + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/load_response.py b/src/square/types/load_response.py new file mode 100644 index 00000000..298735e5 --- /dev/null +++ b/src/square/types/load_response.py @@ -0,0 +1,34 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .load_result import LoadResult + + +class LoadResponse(UncheckedBaseModel): + pivot_query: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, typing.Any]], + FieldMetadata(alias="pivotQuery"), + pydantic.Field(alias="pivotQuery"), + ] = None + slow_query: typing_extensions.Annotated[ + typing.Optional[bool], FieldMetadata(alias="slowQuery"), pydantic.Field(alias="slowQuery") + ] = None + query_type: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="queryType"), pydantic.Field(alias="queryType") + ] = None + results: typing.List[LoadResult] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/load_result.py b/src/square/types/load_result.py new file mode 100644 index 00000000..b64bda48 --- /dev/null +++ b/src/square/types/load_result.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .load_result_annotation import LoadResultAnnotation +from .load_result_data import LoadResultData + + +class LoadResult(UncheckedBaseModel): + data_source: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="dataSource"), pydantic.Field(alias="dataSource") + ] = None + annotation: LoadResultAnnotation + data: LoadResultData + refresh_key_values: typing_extensions.Annotated[ + typing.Optional[typing.List[typing.Dict[str, typing.Any]]], + FieldMetadata(alias="refreshKeyValues"), + pydantic.Field(alias="refreshKeyValues"), + ] = None + last_refresh_time: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="lastRefreshTime"), pydantic.Field(alias="lastRefreshTime") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/load_result_annotation.py b/src/square/types/load_result_annotation.py new file mode 100644 index 00000000..39acef8c --- /dev/null +++ b/src/square/types/load_result_annotation.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class LoadResultAnnotation(UncheckedBaseModel): + measures: typing.Dict[str, typing.Any] + dimensions: typing.Dict[str, typing.Any] + segments: typing.Dict[str, typing.Any] + time_dimensions: typing_extensions.Annotated[ + typing.Dict[str, typing.Any], FieldMetadata(alias="timeDimensions"), pydantic.Field(alias="timeDimensions") + ] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/load_result_data.py b/src/square/types/load_result_data.py new file mode 100644 index 00000000..40079c37 --- /dev/null +++ b/src/square/types/load_result_data.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .load_result_data_columnar import LoadResultDataColumnar +from .load_result_data_compact import LoadResultDataCompact +from .load_result_data_row import LoadResultDataRow + +LoadResultData = typing.Union[LoadResultDataRow, LoadResultDataCompact, LoadResultDataColumnar] diff --git a/src/square/types/load_result_data_columnar.py b/src/square/types/load_result_data_columnar.py new file mode 100644 index 00000000..5ed5a343 --- /dev/null +++ b/src/square/types/load_result_data_columnar.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class LoadResultDataColumnar(UncheckedBaseModel): + """ + Columnar data format - members list paired with one primitive array per column. Returned when `responseFormat=columnar` is requested. + """ + + members: typing.List[str] = pydantic.Field() + """ + Ordered list of member names. Element `i` of `columns` holds the values for `members[i]` across all rows. + """ + + columns: typing.List[typing.List[typing.Any]] = pydantic.Field() + """ + One array per member, in the same order as `members`. Each inner array contains the primitive value of that member for every row (null, boolean, number, string). + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/load_result_data_compact.py b/src/square/types/load_result_data_compact.py new file mode 100644 index 00000000..734192af --- /dev/null +++ b/src/square/types/load_result_data_compact.py @@ -0,0 +1,32 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class LoadResultDataCompact(UncheckedBaseModel): + """ + Compact data format - a single object with the members list and a dataset of primitive arrays. Returned when `responseFormat=compact` is requested. + """ + + members: typing.List[str] = pydantic.Field() + """ + Ordered list of member names that correspond to each cell position in `dataset` rows. + """ + + dataset: typing.List[typing.List[typing.Any]] = pydantic.Field() + """ + Array of rows, where each row is an array of primitive values (null, boolean, number, string) aligned with `members`. + """ + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/load_result_data_row.py b/src/square/types/load_result_data_row.py new file mode 100644 index 00000000..ac37bac6 --- /dev/null +++ b/src/square/types/load_result_data_row.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +LoadResultDataRow = typing.List[typing.Dict[str, typing.Any]] diff --git a/src/square/types/measure.py b/src/square/types/measure.py new file mode 100644 index 00000000..c65a1bf1 --- /dev/null +++ b/src/square/types/measure.py @@ -0,0 +1,52 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .format import Format +from .format_description import FormatDescription + + +class Measure(UncheckedBaseModel): + name: str + title: typing.Optional[str] = None + short_title: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="shortTitle"), pydantic.Field(alias="shortTitle") + ] = None + description: typing.Optional[str] = None + type: str + agg_type: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="aggType"), pydantic.Field(alias="aggType") + ] = None + meta: typing.Optional[typing.Dict[str, typing.Any]] = None + format: typing.Optional[Format] = None + format_description: typing_extensions.Annotated[ + typing.Optional[FormatDescription], + FieldMetadata(alias="formatDescription"), + pydantic.Field(alias="formatDescription"), + ] = None + currency: typing.Optional[str] = pydantic.Field(default=None) + """ + ISO 4217 currency code in uppercase (3 characters, e.g. USD, EUR) + """ + + alias_member: typing_extensions.Annotated[ + typing.Optional[str], + FieldMetadata(alias="aliasMember"), + pydantic.Field( + alias="aliasMember", description="When measure is defined in View, it keeps the original path: Cube.measure" + ), + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/metadata_response.py b/src/square/types/metadata_response.py new file mode 100644 index 00000000..fe106b94 --- /dev/null +++ b/src/square/types/metadata_response.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .cube import Cube + + +class MetadataResponse(UncheckedBaseModel): + cubes: typing.Optional[typing.List[Cube]] = None + compiler_id: typing_extensions.Annotated[ + typing.Optional[str], FieldMetadata(alias="compilerId"), pydantic.Field(alias="compilerId") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/nested_folder.py b/src/square/types/nested_folder.py new file mode 100644 index 00000000..f7bd8775 --- /dev/null +++ b/src/square/types/nested_folder.py @@ -0,0 +1,21 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class NestedFolder(UncheckedBaseModel): + name: str + members: typing.List[str] + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/query.py b/src/square/types/query.py new file mode 100644 index 00000000..59e2a8d8 --- /dev/null +++ b/src/square/types/query.py @@ -0,0 +1,51 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel +from .join_hint import JoinHint +from .join_subquery import JoinSubquery +from .query_filter import QueryFilter +from .response_format import ResponseFormat +from .time_dimension import TimeDimension + + +class Query(UncheckedBaseModel): + measures: typing.Optional[typing.List[str]] = None + dimensions: typing.Optional[typing.List[str]] = None + segments: typing.Optional[typing.List[str]] = None + time_dimensions: typing_extensions.Annotated[ + typing.Optional[typing.List[TimeDimension]], + FieldMetadata(alias="timeDimensions"), + pydantic.Field(alias="timeDimensions"), + ] = None + order: typing.Optional[typing.List[typing.List[str]]] = None + limit: typing.Optional[int] = None + offset: typing.Optional[int] = None + filters: typing.Optional[typing.List[QueryFilter]] = None + ungrouped: typing.Optional[bool] = None + subquery_joins: typing_extensions.Annotated[ + typing.Optional[typing.List[JoinSubquery]], + FieldMetadata(alias="subqueryJoins"), + pydantic.Field(alias="subqueryJoins"), + ] = None + join_hints: typing_extensions.Annotated[ + typing.Optional[typing.List[JoinHint]], FieldMetadata(alias="joinHints"), pydantic.Field(alias="joinHints") + ] = None + timezone: typing.Optional[str] = None + response_format: typing_extensions.Annotated[ + typing.Optional[ResponseFormat], FieldMetadata(alias="responseFormat"), pydantic.Field(alias="responseFormat") + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/query_filter.py b/src/square/types/query_filter.py new file mode 100644 index 00000000..9e37bf92 --- /dev/null +++ b/src/square/types/query_filter.py @@ -0,0 +1,9 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .query_filter_and import QueryFilterAnd +from .query_filter_condition import QueryFilterCondition +from .query_filter_or import QueryFilterOr + +QueryFilter = typing.Union[QueryFilterCondition, QueryFilterOr, QueryFilterAnd] diff --git a/src/square/types/query_filter_and.py b/src/square/types/query_filter_and.py new file mode 100644 index 00000000..cced013d --- /dev/null +++ b/src/square/types/query_filter_and.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class QueryFilterAnd(UncheckedBaseModel): + and_: typing_extensions.Annotated[ + typing.Optional[typing.List[typing.Dict[str, typing.Any]]], + FieldMetadata(alias="and"), + pydantic.Field(alias="and"), + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/query_filter_condition.py b/src/square/types/query_filter_condition.py new file mode 100644 index 00000000..9393d1dd --- /dev/null +++ b/src/square/types/query_filter_condition.py @@ -0,0 +1,22 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class QueryFilterCondition(UncheckedBaseModel): + member: typing.Optional[str] = None + operator: typing.Optional[str] = None + values: typing.Optional[typing.List[str]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/query_filter_or.py b/src/square/types/query_filter_or.py new file mode 100644 index 00000000..238b99ec --- /dev/null +++ b/src/square/types/query_filter_or.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class QueryFilterOr(UncheckedBaseModel): + or_: typing_extensions.Annotated[ + typing.Optional[typing.List[typing.Dict[str, typing.Any]]], + FieldMetadata(alias="or"), + pydantic.Field(alias="or"), + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/reporting_error.py b/src/square/types/reporting_error.py new file mode 100644 index 00000000..e2165989 --- /dev/null +++ b/src/square/types/reporting_error.py @@ -0,0 +1,24 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.unchecked_base_model import UncheckedBaseModel + + +class ReportingError(UncheckedBaseModel): + """ + Error envelope returned by the Reporting API. Note: a 200 response whose body is `{ "error": "Continue wait" }` is not a failure — it signals that a long-running query is still processing and the request should be retried. + """ + + error: str + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/response_format.py b/src/square/types/response_format.py new file mode 100644 index 00000000..28f7e80f --- /dev/null +++ b/src/square/types/response_format.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +ResponseFormat = typing.Union[typing.Literal["default", "compact", "columnar"], typing.Any] diff --git a/src/square/types/segment.py b/src/square/types/segment.py new file mode 100644 index 00000000..05342ee2 --- /dev/null +++ b/src/square/types/segment.py @@ -0,0 +1,26 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class Segment(UncheckedBaseModel): + name: str + title: str + description: typing.Optional[str] = None + short_title: typing_extensions.Annotated[str, FieldMetadata(alias="shortTitle"), pydantic.Field(alias="shortTitle")] + meta: typing.Optional[typing.Dict[str, typing.Any]] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/types/simple_format.py b/src/square/types/simple_format.py new file mode 100644 index 00000000..7d2f83b6 --- /dev/null +++ b/src/square/types/simple_format.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +SimpleFormat = typing.Union[typing.Literal["percent", "currency", "number", "imageUrl", "id", "link"], typing.Any] diff --git a/src/square/types/time_dimension.py b/src/square/types/time_dimension.py new file mode 100644 index 00000000..3f44a097 --- /dev/null +++ b/src/square/types/time_dimension.py @@ -0,0 +1,28 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +import typing_extensions +from ..core.pydantic_utilities import IS_PYDANTIC_V2 +from ..core.serialization import FieldMetadata +from ..core.unchecked_base_model import UncheckedBaseModel + + +class TimeDimension(UncheckedBaseModel): + dimension: str + granularity: typing.Optional[str] = None + date_range: typing_extensions.Annotated[ + typing.Optional[typing.Dict[str, typing.Any]], + FieldMetadata(alias="dateRange"), + pydantic.Field(alias="dateRange"), + ] = None + + if IS_PYDANTIC_V2: + model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 + else: + + class Config: + frozen = True + smart_union = True + extra = pydantic.Extra.allow diff --git a/src/square/utils/reporting_helper.py b/src/square/utils/reporting_helper.py new file mode 100644 index 00000000..2ff6203b --- /dev/null +++ b/src/square/utils/reporting_helper.py @@ -0,0 +1,187 @@ +import asyncio +import threading +import time +import typing + +from ..core.request_options import RequestOptions +from ..requests.query import QueryParams +from ..types.cache_mode import CacheMode +from ..types.load_response import LoadResponse + +if typing.TYPE_CHECKING: + from ..client import AsyncSquare, Square + +# Sentinel returned by the Reporting API on an HTTP 200 while a /v1/load query is +# still processing. It is NOT an error -- the request should be retried. See the +# Reporting API docs: https://developer.squareup.com/docs/reporting-api/overview +CONTINUE_WAIT = "Continue wait" + +# Defaults for the polling loop: up to 20 attempts with exponential backoff +# starting at 2s and capped at 20s. +DEFAULT_MAX_ATTEMPTS = 20 +DEFAULT_INITIAL_DELAY_S = 2.0 +DEFAULT_MAX_DELAY_S = 20.0 +DEFAULT_BACKOFF_FACTOR = 2.0 + + +class ReportingPollTimeoutError(Exception): + """Raised when a reporting query does not resolve within the allotted attempts.""" + + +class ReportingPollCancelledError(Exception): + """Raised when a reporting poll loop is cancelled via its ``cancel_event``.""" + + +def _is_continue_wait(response: LoadResponse) -> bool: + # A "Continue wait" body parses into a LoadResponse (LoadResponse is an + # UncheckedBaseModel, so validation is skipped) with the extra ``error`` field + # preserved (the model is configured with extra="allow") and ``results`` left as + # None. That surviving ``error`` sentinel is the signal to retry. + return getattr(response, "error", None) == CONTINUE_WAIT + + +def _build_load_kwargs( + *, + query_type: typing.Optional[str], + cache: typing.Optional[CacheMode], + query: typing.Optional[QueryParams], + request_options: typing.Optional[RequestOptions], +) -> typing.Dict[str, typing.Any]: + # Forward only the inputs the caller actually set; the generated ``load`` omits + # anything we leave out (its params default to a sentinel), so a ``None`` here + # means "don't send it" rather than "send null". + load_kwargs: typing.Dict[str, typing.Any] = {"request_options": request_options} + if query_type is not None: + load_kwargs["query_type"] = query_type + if cache is not None: + load_kwargs["cache"] = cache + if query is not None: + load_kwargs["query"] = query + return load_kwargs + + +def _next_delay(delay: float, backoff_factor: float, max_delay_s: float) -> float: + return min(delay * backoff_factor, max_delay_s) + + +def _timeout_error(max_attempts: int) -> ReportingPollTimeoutError: + return ReportingPollTimeoutError( + f'Reporting query did not complete after {max_attempts} attempts ("{CONTINUE_WAIT}").' + ) + + +def load_and_wait( + client: "Square", + *, + query_type: typing.Optional[str] = None, + cache: typing.Optional[CacheMode] = None, + query: typing.Optional[QueryParams] = None, + max_attempts: int = DEFAULT_MAX_ATTEMPTS, + initial_delay_s: float = DEFAULT_INITIAL_DELAY_S, + max_delay_s: float = DEFAULT_MAX_DELAY_S, + backoff_factor: float = DEFAULT_BACKOFF_FACTOR, + cancel_event: typing.Optional[threading.Event] = None, + request_options: typing.Optional[RequestOptions] = None, +) -> LoadResponse: + """ + Runs a reporting query and transparently polls until it resolves, returning the + final ``LoadResponse``. Re-sends the identical request with exponential backoff + while the Reporting API answers "Continue wait". + + Args: + client: A configured synchronous ``Square`` client. + query_type: Optional query type (passed through to ``client.reporting.load``). + cache: Optional cache strategy. + query: The reporting query (measures, dimensions, filters, ...). + max_attempts: Maximum poll attempts before giving up. Default 20. + initial_delay_s: Delay before the first retry, in seconds. Default 2.0. + max_delay_s: Upper bound on the backoff delay, in seconds. Default 20.0. + backoff_factor: Multiplier applied to the delay after each attempt. Default 2.0. + cancel_event: Optional ``threading.Event``; when set, the loop stops promptly + (interrupting any in-flight backoff sleep) and raises + ``ReportingPollCancelledError``. + request_options: Forwarded to each underlying ``client.reporting.load`` call. + + Returns: + The resolved ``LoadResponse`` (never the "Continue wait" sentinel). + + Raises: + ReportingPollTimeoutError: if the query does not resolve within ``max_attempts``. + ReportingPollCancelledError: if ``cancel_event`` is set before the query resolves. + """ + load_kwargs = _build_load_kwargs( + query_type=query_type, cache=cache, query=query, request_options=request_options + ) + delay = initial_delay_s + for attempt in range(1, max_attempts + 1): + if cancel_event is not None and cancel_event.is_set(): + raise ReportingPollCancelledError("Reporting query polling was cancelled.") + response = client.reporting.load(**load_kwargs) + if not _is_continue_wait(response): + return response + if attempt == max_attempts: + break + # ``Event.wait`` doubles as an interruptible sleep: it returns True as soon as + # the event is set, so cancellation does not wait out the remaining backoff. + if cancel_event is not None: + if cancel_event.wait(delay): + raise ReportingPollCancelledError("Reporting query polling was cancelled.") + else: + time.sleep(delay) + delay = _next_delay(delay, backoff_factor, max_delay_s) + + raise _timeout_error(max_attempts) + + +async def load_and_wait_async( + client: "AsyncSquare", + *, + query_type: typing.Optional[str] = None, + cache: typing.Optional[CacheMode] = None, + query: typing.Optional[QueryParams] = None, + max_attempts: int = DEFAULT_MAX_ATTEMPTS, + initial_delay_s: float = DEFAULT_INITIAL_DELAY_S, + max_delay_s: float = DEFAULT_MAX_DELAY_S, + backoff_factor: float = DEFAULT_BACKOFF_FACTOR, + request_options: typing.Optional[RequestOptions] = None, +) -> LoadResponse: + """ + Async counterpart to :func:`load_and_wait` for the ``AsyncSquare`` client. Polls + ``client.reporting.load`` with exponential backoff while the Reporting API answers + "Continue wait", returning the resolved ``LoadResponse``. + + Cancellation is handled the idiomatic asyncio way: cancel the awaiting task (e.g. + via ``asyncio.wait_for`` / ``Task.cancel``) and the in-flight ``asyncio.sleep`` + raises ``asyncio.CancelledError``, which propagates out of this coroutine. + + Args: + client: A configured ``AsyncSquare`` client. + query_type: Optional query type (passed through to ``client.reporting.load``). + cache: Optional cache strategy. + query: The reporting query (measures, dimensions, filters, ...). + max_attempts: Maximum poll attempts before giving up. Default 20. + initial_delay_s: Delay before the first retry, in seconds. Default 2.0. + max_delay_s: Upper bound on the backoff delay, in seconds. Default 20.0. + backoff_factor: Multiplier applied to the delay after each attempt. Default 2.0. + request_options: Forwarded to each underlying ``client.reporting.load`` call. + + Returns: + The resolved ``LoadResponse`` (never the "Continue wait" sentinel). + + Raises: + ReportingPollTimeoutError: if the query does not resolve within ``max_attempts``. + """ + load_kwargs = _build_load_kwargs( + query_type=query_type, cache=cache, query=query, request_options=request_options + ) + delay = initial_delay_s + for attempt in range(1, max_attempts + 1): + response = await client.reporting.load(**load_kwargs) + if not _is_continue_wait(response): + return response + if attempt == max_attempts: + break + await asyncio.sleep(delay) + delay = _next_delay(delay, backoff_factor, max_delay_s) + + raise _timeout_error(max_attempts) diff --git a/tests/integration/test_reporting.py b/tests/integration/test_reporting.py new file mode 100644 index 00000000..680a1abf --- /dev/null +++ b/tests/integration/test_reporting.py @@ -0,0 +1,92 @@ +"""Live, end-to-end tests for the Reporting API. + +The Reporting API is a beta, bespoke offering served ONLY from production +(connect.squareup.com/reporting) -- it is not routed on sandbox (which 404s), +and a sandbox token 401s against prod. Validating it live therefore needs a +production, reporting-provisioned access token. CI's regular ``TEST_SQUARE_TOKEN`` +is sandbox-only, so this suite is gated behind ``TEST_SQUARE_REPORTING`` -- which +is itself the prod, reporting-provisioned token -- and skips by default when it is +unset, keeping CI green. The endpoints exercised are read-only (schema +discovery + queries). The polling *logic* is covered without a live account in +``test_reporting_helper.py``. + +Run it against a real prod account: + + TEST_SQUARE_REPORTING= \ + poetry run pytest tests/integration/test_reporting.py + # Override the host with TEST_SQUARE_BASE_URL= if reporting moves. +""" + +import os + +import pytest + +from square import Square +from square.environment import SquareEnvironment +from square.utils.reporting_helper import load_and_wait + +pytestmark = pytest.mark.skipif( + not os.getenv("TEST_SQUARE_REPORTING"), + reason="Set TEST_SQUARE_REPORTING to a prod, reporting-provisioned token to run the live Reporting API suite.", +) + + +def reporting_client() -> Square: + token = os.getenv("TEST_SQUARE_REPORTING") + if not token: + raise RuntimeError("TEST_SQUARE_REPORTING must be set to a prod, reporting-provisioned token to run the reporting integration suite.") + # Reporting only exists on production; allow overriding the host via TEST_SQUARE_BASE_URL. + base_url = os.getenv("TEST_SQUARE_BASE_URL") + if base_url: + return Square(token=token, base_url=base_url) + return Square(token=token, environment=SquareEnvironment.PRODUCTION) + + +def first_measure_name(client: Square) -> str: + metadata = client.reporting.get_metadata() + cubes = metadata.cubes or [] + for cube in cubes: + for measure in cube.measures or []: + if measure.name: + return measure.name + raise RuntimeError("No cubes/measures are available on the reporting schema for this account.") + + +def test_get_metadata_returns_queryable_schema() -> None: + client = reporting_client() + metadata = client.reporting.get_metadata() + + assert metadata.cubes is not None + assert len(metadata.cubes) > 0 + + +def test_load_returns_results_or_continue_wait_sentinel() -> None: + client = reporting_client() + measure = first_measure_name(client) + + response = client.reporting.load(query={"measures": [measure]}) + + sentinel = getattr(response, "error", None) + if sentinel is not None: + # Documented async behavior: a still-processing query comes back as HTTP 200 + # with {"error": "Continue wait"} instead of results. + assert sentinel == "Continue wait" + else: + assert response.results is not None + + +def test_load_and_wait_resolves_without_continue_wait() -> None: + client = reporting_client() + measure = first_measure_name(client) + + response = load_and_wait( + client, + query={"measures": [measure]}, + max_attempts=20, + initial_delay_s=2.0, + max_delay_s=20.0, + ) + + # The polling helper must never hand back the raw "Continue wait" sentinel. + assert getattr(response, "error", None) is None + assert response.results is not None diff --git a/tests/integration/test_reporting_helper.py b/tests/integration/test_reporting_helper.py new file mode 100644 index 00000000..90067c1c --- /dev/null +++ b/tests/integration/test_reporting_helper.py @@ -0,0 +1,190 @@ +"""Offline unit tests for the Reporting API polling helper. + +The Reporting API answers a still-processing ``/v1/load`` query with an HTTP 200 +whose body is ``{"error": "Continue wait"}``. ``load_and_wait`` / +``load_and_wait_async`` own the retry loop around that sentinel. These tests +exercise that loop without a network by scripting ``client.reporting.load``, plus +one test that proves the sentinel actually survives the generated client's +deserialization. They run offline, so they stay green in CI. + +The live, end-to-end suite lives in ``test_reporting.py`` (gated behind +``TEST_SQUARE_REPORTING``). +""" + +import threading +import typing + +import pytest + +from square.core.unchecked_base_model import construct_type +from square.types.load_response import LoadResponse +from square.utils.reporting_helper import ( + CONTINUE_WAIT, + ReportingPollCancelledError, + ReportingPollTimeoutError, + load_and_wait, + load_and_wait_async, +) + +if typing.TYPE_CHECKING: + from square.client import AsyncSquare, Square + + +def _continue_wait() -> LoadResponse: + # Build the sentinel the same way the generated client does, so the tests + # exercise the real type rather than a hand-rolled stand-in. + return typing.cast(LoadResponse, construct_type(type_=LoadResponse, object_={"error": CONTINUE_WAIT})) + + +def _resolved() -> LoadResponse: + return typing.cast( + LoadResponse, + construct_type(type_=LoadResponse, object_={"results": [{"data": {"Orders.count": "128"}}]}), + ) + + +class _FakeReporting: + """A ``reporting`` stub whose ``load`` returns a scripted sequence of responses.""" + + def __init__( + self, + sequence: typing.List[LoadResponse], + on_call: typing.Optional[typing.Callable[[int], None]] = None, + ) -> None: + self._sequence = sequence + self._on_call = on_call + self.calls = 0 + + def load(self, **_kwargs: typing.Any) -> LoadResponse: + response = self._sequence[min(self.calls, len(self._sequence) - 1)] + self.calls += 1 + if self._on_call is not None: + self._on_call(self.calls) + return response + + +class _FakeAsyncReporting: + def __init__(self, sequence: typing.List[LoadResponse]) -> None: + self._sequence = sequence + self.calls = 0 + + async def load(self, **_kwargs: typing.Any) -> LoadResponse: + response = self._sequence[min(self.calls, len(self._sequence) - 1)] + self.calls += 1 + return response + + +def _fake_client(reporting: typing.Any) -> "Square": + class _FakeClient: + pass + + client = _FakeClient() + client.reporting = reporting # type: ignore[attr-defined] + return typing.cast("Square", client) + + +def test_polls_past_continue_wait_and_returns_resolved_result() -> None: + reporting = _FakeReporting([_continue_wait(), _continue_wait(), _resolved()]) + client = _fake_client(reporting) + + response = load_and_wait( + client, + query={"measures": ["Orders.count"]}, + initial_delay_s=0.001, + max_delay_s=0.001, + max_attempts=5, + ) + + # The helper must never hand back the raw sentinel. + assert getattr(response, "error", None) is None + assert response.results is not None + assert reporting.calls == 3 + + +def test_returns_immediately_when_first_response_has_results() -> None: + reporting = _FakeReporting([_resolved()]) + client = _fake_client(reporting) + + response = load_and_wait(client, initial_delay_s=0.001) + + assert response.results is not None + assert reporting.calls == 1 + + +def test_raises_timeout_once_max_attempts_exhausted() -> None: + reporting = _FakeReporting([_continue_wait()]) # never resolves + client = _fake_client(reporting) + + with pytest.raises(ReportingPollTimeoutError, match="did not complete after 3 attempts"): + load_and_wait(client, initial_delay_s=0.001, max_delay_s=0.001, max_attempts=3) + + assert reporting.calls == 3 + + +def test_cancel_event_set_before_start_aborts_without_calling() -> None: + reporting = _FakeReporting([_continue_wait()]) + client = _fake_client(reporting) + cancel_event = threading.Event() + cancel_event.set() + + with pytest.raises(ReportingPollCancelledError): + load_and_wait(client, initial_delay_s=10, max_attempts=10, cancel_event=cancel_event) + + assert reporting.calls == 0 + + +def test_cancel_event_interrupts_backoff_sleep() -> None: + cancel_event = threading.Event() + + # Trip the event after the first poll; the helper's interruptible backoff sleep + # must then return promptly and raise rather than waiting out the delay. + def on_call(_count: int) -> None: + cancel_event.set() + + reporting = _FakeReporting([_continue_wait()], on_call=on_call) + client = _fake_client(reporting) + + with pytest.raises(ReportingPollCancelledError): + load_and_wait(client, initial_delay_s=30, max_attempts=10, cancel_event=cancel_event) + + assert reporting.calls == 1 + + +def test_continue_wait_body_survives_real_deserialization_as_sentinel() -> None: + # The crux of the design: the generated ``reporting.load`` parses the body with + # ``construct_type`` (skip-validation + extra="allow"), so the ``error`` sentinel + # survives onto a LoadResponse-shaped object while ``results`` stays None. If this + # ever stops being true, load_and_wait would mistake "Continue wait" for a result. + parsed = typing.cast( + typing.Any, construct_type(type_=LoadResponse, object_={"error": CONTINUE_WAIT}) + ) + + assert parsed.error == CONTINUE_WAIT + assert parsed.results is None + + +async def test_async_polls_past_continue_wait_and_resolves() -> None: + reporting = _FakeAsyncReporting([_continue_wait(), _continue_wait(), _resolved()]) + client = typing.cast("AsyncSquare", _fake_client(reporting)) + + response = await load_and_wait_async( + client, + query={"measures": ["Orders.count"]}, + initial_delay_s=0.001, + max_delay_s=0.001, + max_attempts=5, + ) + + assert getattr(response, "error", None) is None + assert response.results is not None + assert reporting.calls == 3 + + +async def test_async_raises_timeout_once_max_attempts_exhausted() -> None: + reporting = _FakeAsyncReporting([_continue_wait()]) # never resolves + client = typing.cast("AsyncSquare", _fake_client(reporting)) + + with pytest.raises(ReportingPollTimeoutError, match="did not complete after 3 attempts"): + await load_and_wait_async(client, initial_delay_s=0.001, max_delay_s=0.001, max_attempts=3) + + assert reporting.calls == 3