diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 511dd51..2f8909f 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.6.0" + ".": "2.6.1" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 93a1821..c91eb54 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 6 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-5e6e6508dca15391d72cbea74ec33838c511cbc17e046699142f97d1573b069a.yml -openapi_spec_hash: b73b5922a495fd375736896912815d18 -config_hash: 57c64e5e8fe99c1bd7af536d82af4ad9 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla/tabstack-c97fe4d7e39fd2f35a75dc7fdeb6d3f19aab2f9b56d4983c3376a5433dc9e268.yml +openapi_spec_hash: 8a4612fa101f82893c6edca118a86a2d +config_hash: 5827ec0cdbbac0e7c5db2f5456f67dca diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bfb6c6..9a90f1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 2.6.1 (2026-05-05) + +Full Changelog: [v2.6.0...v2.6.1](https://github.com/Mozilla-Ocho/tabstack-python/compare/v2.6.0...v2.6.1) + +### Features + +* **api:** add endpoint specfic timeouts ([15bcc6b](https://github.com/Mozilla-Ocho/tabstack-python/commit/15bcc6b29cf0e64fa5bf544f099471dac26d9cea)) +* **api:** manual updates ([e89cb7a](https://github.com/Mozilla-Ocho/tabstack-python/commit/e89cb7a58d7e139cb46d54f15fbb26515a54f205)) +* **automate:** emit task:trace_context SSE event with trace ID ([20d3ff3](https://github.com/Mozilla-Ocho/tabstack-python/commit/20d3ff3b4c5e8d7d287ef9807ecbc33c522bed37)) +* support setting headers via env ([4699481](https://github.com/Mozilla-Ocho/tabstack-python/commit/4699481aee8341952e2cae287bc0ff0fc629a292)) + + +### Bug Fixes + +* use correct field name format for multipart file arrays ([a10a1ed](https://github.com/Mozilla-Ocho/tabstack-python/commit/a10a1ed1298179229f69dc3c12b899f6242ed1b4)) + + +### Chores + +* **internal:** reformat pyproject.toml ([c909f52](https://github.com/Mozilla-Ocho/tabstack-python/commit/c909f52bf36e746213c2cb8b316e6eb1909a2e80)) + ## 2.6.0 (2026-04-24) Full Changelog: [v2.5.0...v2.6.0](https://github.com/Mozilla-Ocho/tabstack-python/compare/v2.5.0...v2.6.0) diff --git a/api.md b/api.md index 51b78b1..c9a0a65 100644 --- a/api.md +++ b/api.md @@ -1,9 +1,21 @@ +# Shared Types + +```python +from tabstack.types import GeotargetGeoTarget +``` + # Agent Types: ```python -from tabstack.types import AutomateEvent, ResearchEvent, AgentAutomateInputResponse +from tabstack.types import ( + AutomateEvent, + ResearchEvent, + V1GlobalBuffer, + V1ResearchQuestionAssessment, + AgentAutomateInputResponse, +) ``` Methods: diff --git a/pyproject.toml b/pyproject.toml index 3050875..92a2355 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tabstack" -version = "2.6.0" +version = "2.6.1" description = "The official Python library for the tabstack API" dynamic = ["readme"] license = "Apache-2.0" @@ -154,7 +154,7 @@ show_error_codes = true # # We also exclude our `tests` as mypy doesn't always infer # types correctly and Pyright will still catch any type errors. -exclude = ['src/tabstack/_files.py', '_dev/.*.py', 'tests/.*'] +exclude = ["src/tabstack/_files.py", "_dev/.*.py", "tests/.*"] strict_equality = true implicit_reexport = true diff --git a/src/tabstack/_client.py b/src/tabstack/_client.py index 9085397..1c3577e 100644 --- a/src/tabstack/_client.py +++ b/src/tabstack/_client.py @@ -19,7 +19,11 @@ RequestOptions, not_given, ) -from ._utils import is_given, get_async_library +from ._utils import ( + is_given, + is_mapping_t, + get_async_library, +) from ._compat import cached_property from ._version import __version__ from ._streaming import Stream as Stream, AsyncStream as AsyncStream @@ -92,6 +96,15 @@ def __init__( if base_url is None: base_url = f"https://api.tabstack.ai/v1" + custom_headers_env = os.environ.get("TABSTACK_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, @@ -278,6 +291,15 @@ def __init__( if base_url is None: base_url = f"https://api.tabstack.ai/v1" + custom_headers_env = os.environ.get("TABSTACK_CUSTOM_HEADERS") + if custom_headers_env is not None: + parsed: dict[str, str] = {} + for line in custom_headers_env.split("\n"): + colon = line.find(":") + if colon >= 0: + parsed[line[:colon].strip()] = line[colon + 1 :].strip() + default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})} + super().__init__( version=__version__, base_url=base_url, diff --git a/src/tabstack/_qs.py b/src/tabstack/_qs.py index de8c99b..4127c19 100644 --- a/src/tabstack/_qs.py +++ b/src/tabstack/_qs.py @@ -2,17 +2,13 @@ from typing import Any, List, Tuple, Union, Mapping, TypeVar from urllib.parse import parse_qs, urlencode -from typing_extensions import Literal, get_args +from typing_extensions import get_args -from ._types import NotGiven, not_given +from ._types import NotGiven, ArrayFormat, NestedFormat, not_given from ._utils import flatten _T = TypeVar("_T") - -ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] -NestedFormat = Literal["dots", "brackets"] - PrimitiveData = Union[str, int, float, bool, None] # this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"] # https://github.com/microsoft/pyright/issues/3555 diff --git a/src/tabstack/_types.py b/src/tabstack/_types.py index 818a79d..7e89ea2 100644 --- a/src/tabstack/_types.py +++ b/src/tabstack/_types.py @@ -47,6 +47,9 @@ ModelT = TypeVar("ModelT", bound=pydantic.BaseModel) _T = TypeVar("_T") +ArrayFormat = Literal["comma", "repeat", "indices", "brackets"] +NestedFormat = Literal["dots", "brackets"] + # Approximates httpx internal ProxiesTypes and RequestFiles types # while adding support for `PathLike` instances diff --git a/src/tabstack/_utils/_utils.py b/src/tabstack/_utils/_utils.py index 771859f..199cd23 100644 --- a/src/tabstack/_utils/_utils.py +++ b/src/tabstack/_utils/_utils.py @@ -17,11 +17,11 @@ ) from pathlib import Path from datetime import date, datetime -from typing_extensions import TypeGuard +from typing_extensions import TypeGuard, get_args import sniffio -from .._types import Omit, NotGiven, FileTypes, HeadersLike +from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike _T = TypeVar("_T") _TupleT = TypeVar("_TupleT", bound=Tuple[object, ...]) @@ -40,25 +40,45 @@ def extract_files( query: Mapping[str, object], *, paths: Sequence[Sequence[str]], + array_format: ArrayFormat = "brackets", ) -> list[tuple[str, FileTypes]]: """Recursively extract files from the given dictionary based on specified paths. A path may look like this ['foo', 'files', '', 'data']. + ``array_format`` controls how ```` segments contribute to the emitted + field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and + ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``). + Note: this mutates the given dictionary. """ files: list[tuple[str, FileTypes]] = [] for path in paths: - files.extend(_extract_items(query, path, index=0, flattened_key=None)) + files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format)) return files +def _array_suffix(array_format: ArrayFormat, array_index: int) -> str: + if array_format == "brackets": + return "[]" + if array_format == "indices": + return f"[{array_index}]" + if array_format == "repeat" or array_format == "comma": + # Both repeat the bare field name for each file part; there is no + # meaningful way to comma-join binary parts. + return "" + raise NotImplementedError( + f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}" + ) + + def _extract_items( obj: object, path: Sequence[str], *, index: int, flattened_key: str | None, + array_format: ArrayFormat, ) -> list[tuple[str, FileTypes]]: try: key = path[index] @@ -75,9 +95,11 @@ def _extract_items( if is_list(obj): files: list[tuple[str, FileTypes]] = [] - for entry in obj: - assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "") - files.append((flattened_key + "[]", cast(FileTypes, entry))) + for array_index, entry in enumerate(obj): + suffix = _array_suffix(array_format, array_index) + emitted_key = (flattened_key + suffix) if flattened_key else suffix + assert_is_file_content(entry, key=emitted_key) + files.append((emitted_key, cast(FileTypes, entry))) return files assert_is_file_content(obj, key=flattened_key) @@ -106,6 +128,7 @@ def _extract_items( path, index=index, flattened_key=flattened_key, + array_format=array_format, ) elif is_list(obj): if key != "": @@ -117,9 +140,12 @@ def _extract_items( item, path, index=index, - flattened_key=flattened_key + "[]" if flattened_key is not None else "[]", + flattened_key=( + (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index) + ), + array_format=array_format, ) - for item in obj + for array_index, item in enumerate(obj) ] ) diff --git a/src/tabstack/_version.py b/src/tabstack/_version.py index 5187085..f9930e7 100644 --- a/src/tabstack/_version.py +++ b/src/tabstack/_version.py @@ -1,4 +1,4 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. __title__ = "tabstack" -__version__ = "2.6.0" # x-release-please-version +__version__ = "2.6.1" # x-release-please-version diff --git a/src/tabstack/resources/agent.py b/src/tabstack/resources/agent.py index 01a80cb..198a843 100644 --- a/src/tabstack/resources/agent.py +++ b/src/tabstack/resources/agent.py @@ -9,7 +9,7 @@ from ..types import agent_automate_params, agent_research_params, agent_automate_input_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import path_template, maybe_transform, async_maybe_transform +from .._utils import is_given, path_template, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -18,11 +18,13 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._constants import DEFAULT_TIMEOUT from .._streaming import Stream, AsyncStream from .._base_client import make_request_options from ..types.automate_event import AutomateEvent from ..types.research_event import ResearchEvent from ..types.agent_automate_input_response import AgentAutomateInputResponse +from ..types.shared_params.geotarget_geo_target import GeotargetGeoTarget __all__ = ["AgentResource", "AsyncAgentResource"] @@ -52,7 +54,7 @@ def automate( *, task: str, data: object | Omit = omit, - geo_target: agent_automate_params.GeoTarget | Omit = omit, + geo_target: GeotargetGeoTarget | Omit = omit, guardrails: str | Omit = omit, interactive: bool | Omit = omit, max_iterations: int | Omit = omit, @@ -112,6 +114,8 @@ def automate( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._post( "/automate", @@ -250,6 +254,8 @@ def research( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return self._post( "/research", @@ -300,7 +306,7 @@ async def automate( *, task: str, data: object | Omit = omit, - geo_target: agent_automate_params.GeoTarget | Omit = omit, + geo_target: GeotargetGeoTarget | Omit = omit, guardrails: str | Omit = omit, interactive: bool | Omit = omit, max_iterations: int | Omit = omit, @@ -360,6 +366,8 @@ async def automate( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._post( "/automate", @@ -498,6 +506,8 @@ async def research( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 600 extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} return await self._post( "/research", diff --git a/src/tabstack/resources/extract.py b/src/tabstack/resources/extract.py index 06f0ff3..19914ee 100644 --- a/src/tabstack/resources/extract.py +++ b/src/tabstack/resources/extract.py @@ -8,7 +8,7 @@ from ..types import extract_json_params, extract_markdown_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import is_given, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -17,9 +17,11 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._constants import DEFAULT_TIMEOUT from .._base_client import make_request_options from ..types.extract_json_response import ExtractJsonResponse from ..types.extract_markdown_response import ExtractMarkdownResponse +from ..types.shared_params.geotarget_geo_target import GeotargetGeoTarget __all__ = ["ExtractResource", "AsyncExtractResource"] @@ -50,7 +52,7 @@ def json( json_schema: object, url: str, effort: Literal["min", "standard", "max"] | Omit = omit, - geo_target: extract_json_params.GeoTarget | Omit = omit, + geo_target: GeotargetGeoTarget | Omit = omit, nocache: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -83,6 +85,8 @@ def json( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 300 return self._post( "/extract/json", body=maybe_transform( @@ -106,7 +110,7 @@ def markdown( *, url: str, effort: Literal["min", "standard", "max"] | Omit = omit, - geo_target: extract_markdown_params.GeoTarget | Omit = omit, + geo_target: GeotargetGeoTarget | Omit = omit, metadata: bool | Omit = omit, nocache: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -142,6 +146,8 @@ def markdown( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 180 return self._post( "/extract/markdown", body=maybe_transform( @@ -187,7 +193,7 @@ async def json( json_schema: object, url: str, effort: Literal["min", "standard", "max"] | Omit = omit, - geo_target: extract_json_params.GeoTarget | Omit = omit, + geo_target: GeotargetGeoTarget | Omit = omit, nocache: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -220,6 +226,8 @@ async def json( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 300 return await self._post( "/extract/json", body=await async_maybe_transform( @@ -243,7 +251,7 @@ async def markdown( *, url: str, effort: Literal["min", "standard", "max"] | Omit = omit, - geo_target: extract_markdown_params.GeoTarget | Omit = omit, + geo_target: GeotargetGeoTarget | Omit = omit, metadata: bool | Omit = omit, nocache: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. @@ -279,6 +287,8 @@ async def markdown( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 180 return await self._post( "/extract/markdown", body=await async_maybe_transform( diff --git a/src/tabstack/resources/generate.py b/src/tabstack/resources/generate.py index b9a2996..d990445 100644 --- a/src/tabstack/resources/generate.py +++ b/src/tabstack/resources/generate.py @@ -8,7 +8,7 @@ from ..types import generate_json_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given -from .._utils import maybe_transform, async_maybe_transform +from .._utils import is_given, maybe_transform, async_maybe_transform from .._compat import cached_property from .._resource import SyncAPIResource, AsyncAPIResource from .._response import ( @@ -17,8 +17,10 @@ async_to_raw_response_wrapper, async_to_streamed_response_wrapper, ) +from .._constants import DEFAULT_TIMEOUT from .._base_client import make_request_options from ..types.generate_json_response import GenerateJsonResponse +from ..types.shared_params.geotarget_geo_target import GeotargetGeoTarget __all__ = ["GenerateResource", "AsyncGenerateResource"] @@ -50,7 +52,7 @@ def json( json_schema: object, url: str, effort: Literal["min", "standard", "max"] | Omit = omit, - geo_target: generate_json_params.GeoTarget | Omit = omit, + geo_target: GeotargetGeoTarget | Omit = omit, nocache: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -86,6 +88,8 @@ def json( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 300 return self._post( "/generate/json", body=maybe_transform( @@ -133,7 +137,7 @@ async def json( json_schema: object, url: str, effort: Literal["min", "standard", "max"] | Omit = omit, - geo_target: generate_json_params.GeoTarget | Omit = omit, + geo_target: GeotargetGeoTarget | Omit = omit, nocache: bool | Omit = omit, # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs. # The extra values given here take precedence over values defined on the client or passed to this method. @@ -169,6 +173,8 @@ async def json( timeout: Override the client-level default timeout for this request, in seconds """ + if not is_given(timeout) and self._client.timeout == DEFAULT_TIMEOUT: + timeout = 300 return await self._post( "/generate/json", body=await async_maybe_transform( diff --git a/src/tabstack/types/__init__.py b/src/tabstack/types/__init__.py index feec32c..83aab21 100644 --- a/src/tabstack/types/__init__.py +++ b/src/tabstack/types/__init__.py @@ -2,8 +2,10 @@ from __future__ import annotations +from .shared import GeotargetGeoTarget as GeotargetGeoTarget from .automate_event import AutomateEvent as AutomateEvent from .research_event import ResearchEvent as ResearchEvent +from .v1_global_buffer import V1GlobalBuffer as V1GlobalBuffer from .extract_json_params import ExtractJsonParams as ExtractJsonParams from .generate_json_params import GenerateJsonParams as GenerateJsonParams from .agent_automate_params import AgentAutomateParams as AgentAutomateParams @@ -14,3 +16,4 @@ from .extract_markdown_response import ExtractMarkdownResponse as ExtractMarkdownResponse from .agent_automate_input_params import AgentAutomateInputParams as AgentAutomateInputParams from .agent_automate_input_response import AgentAutomateInputResponse as AgentAutomateInputResponse +from .v1_research_question_assessment import V1ResearchQuestionAssessment as V1ResearchQuestionAssessment diff --git a/src/tabstack/types/agent_automate_params.py b/src/tabstack/types/agent_automate_params.py index 3d63b9c..583ac5a 100644 --- a/src/tabstack/types/agent_automate_params.py +++ b/src/tabstack/types/agent_automate_params.py @@ -5,8 +5,9 @@ from typing_extensions import Required, Annotated, TypedDict from .._utils import PropertyInfo +from .shared_params.geotarget_geo_target import GeotargetGeoTarget -__all__ = ["AgentAutomateParams", "GeoTarget"] +__all__ = ["AgentAutomateParams"] class AgentAutomateParams(TypedDict, total=False): @@ -16,7 +17,7 @@ class AgentAutomateParams(TypedDict, total=False): data: object """JSON data to provide context for form filling or complex tasks""" - geo_target: GeoTarget + geo_target: GeotargetGeoTarget """Optional geotargeting parameters for proxy requests""" guardrails: str @@ -33,13 +34,3 @@ class AgentAutomateParams(TypedDict, total=False): url: str """Starting URL for the task""" - - -class GeoTarget(TypedDict, total=False): - """Optional geotargeting parameters for proxy requests""" - - country: str - """ - Country code using ISO 3166-1 alpha-2 standard (2 letters, e.g., "US", "GB", - "JP"). See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - """ diff --git a/src/tabstack/types/automate_event.py b/src/tabstack/types/automate_event.py index 388dba2..e9c3ac5 100644 --- a/src/tabstack/types/automate_event.py +++ b/src/tabstack/types/automate_event.py @@ -8,6 +8,7 @@ from .._utils import PropertyInfo from .._models import BaseModel +from .v1_global_buffer import V1GlobalBuffer __all__ = [ "AutomateEvent", @@ -38,15 +39,11 @@ "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageUnionMember1", "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageUnionMember1Buffer", "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageByteLength", - "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageV1GlobalBuffer", - "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageV1GlobalBufferBuffer", "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1File", "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileData", "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataUnionMember1", "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataUnionMember1Buffer", "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataByteLength", - "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataV1GlobalBuffer", - "V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataV1GlobalBufferBuffer", "V1AutomateEventAIGenerationDataMessageAssistant", "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1", "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1Text", @@ -55,8 +52,6 @@ "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataUnionMember1", "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataUnionMember1Buffer", "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataByteLength", - "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataV1GlobalBuffer", - "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataV1GlobalBufferBuffer", "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1Reasoning", "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1ToolCall", "V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1ToolResult", @@ -147,6 +142,8 @@ "V1AutomateEventTaskSetupData", "V1AutomateEventTaskStarted", "V1AutomateEventTaskStartedData", + "V1AutomateEventTaskTraceContext", + "V1AutomateEventTaskTraceContextData", "V1AutomateEventTaskValidated", "V1AutomateEventTaskValidatedData", "V1AutomateEventTaskValidationError", @@ -395,39 +392,11 @@ class V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageByt byte_length: float = FieldInfo(alias="byteLength") -class V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageV1GlobalBufferBuffer(BaseModel): - byte_length: float = FieldInfo(alias="byteLength") - - -class V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageV1GlobalBuffer(BaseModel): - buffer: V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageV1GlobalBufferBuffer - - byte_length: float = FieldInfo(alias="byteLength") - - byte_offset: float = FieldInfo(alias="byteOffset") - - bytes_per_element: float = FieldInfo(alias="BYTES_PER_ELEMENT") - - length: float - - if TYPE_CHECKING: - # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a - # value to this field, so for compatibility we avoid doing it at runtime. - __pydantic_extra__: Dict[str, float] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] - - # Stub to indicate that arbitrary properties are accepted. - # To access properties that are not valid identifiers you can use `getattr`, e.g. - # `getattr(obj, '$type')` - def __getattr__(self, attr: str) -> float: ... - else: - __pydantic_extra__: Dict[str, float] - - V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImage: TypeAlias = Union[ str, V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageUnionMember1, V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageByteLength, - V1AutomateEventAIGenerationDataMessageUserContentUnionMember1ImageImageV1GlobalBuffer, + V1GlobalBuffer, ] @@ -500,39 +469,11 @@ class V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataByteL byte_length: float = FieldInfo(alias="byteLength") -class V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataV1GlobalBufferBuffer(BaseModel): - byte_length: float = FieldInfo(alias="byteLength") - - -class V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataV1GlobalBuffer(BaseModel): - buffer: V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataV1GlobalBufferBuffer - - byte_length: float = FieldInfo(alias="byteLength") - - byte_offset: float = FieldInfo(alias="byteOffset") - - bytes_per_element: float = FieldInfo(alias="BYTES_PER_ELEMENT") - - length: float - - if TYPE_CHECKING: - # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a - # value to this field, so for compatibility we avoid doing it at runtime. - __pydantic_extra__: Dict[str, float] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] - - # Stub to indicate that arbitrary properties are accepted. - # To access properties that are not valid identifiers you can use `getattr`, e.g. - # `getattr(obj, '$type')` - def __getattr__(self, attr: str) -> float: ... - else: - __pydantic_extra__: Dict[str, float] - - V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileData: TypeAlias = Union[ str, V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataUnionMember1, V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataByteLength, - V1AutomateEventAIGenerationDataMessageUserContentUnionMember1FileDataV1GlobalBuffer, + V1GlobalBuffer, ] @@ -678,39 +619,11 @@ class V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileData byte_length: float = FieldInfo(alias="byteLength") -class V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataV1GlobalBufferBuffer(BaseModel): - byte_length: float = FieldInfo(alias="byteLength") - - -class V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataV1GlobalBuffer(BaseModel): - buffer: V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataV1GlobalBufferBuffer - - byte_length: float = FieldInfo(alias="byteLength") - - byte_offset: float = FieldInfo(alias="byteOffset") - - bytes_per_element: float = FieldInfo(alias="BYTES_PER_ELEMENT") - - length: float - - if TYPE_CHECKING: - # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a - # value to this field, so for compatibility we avoid doing it at runtime. - __pydantic_extra__: Dict[str, float] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] - - # Stub to indicate that arbitrary properties are accepted. - # To access properties that are not valid identifiers you can use `getattr`, e.g. - # `getattr(obj, '$type')` - def __getattr__(self, attr: str) -> float: ... - else: - __pydantic_extra__: Dict[str, float] - - V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileData: TypeAlias = Union[ str, V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataUnionMember1, V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataByteLength, - V1AutomateEventAIGenerationDataMessageAssistantContentUnionMember1FileDataV1GlobalBuffer, + V1GlobalBuffer, ] @@ -2550,6 +2463,30 @@ class V1AutomateEventTaskStarted(BaseModel): event: Literal["task:started"] +class V1AutomateEventTaskTraceContextData(BaseModel): + """Payload for the task:trace_context event. + + Carries the OpenTelemetry trace ID for this /v1/automate request so consumers can deep-link to distributed-tracing UIs (e.g. Cloud Trace, Cloud Logging) for the run. + """ + + trace_id: str = FieldInfo(alias="traceId") + """W3C trace ID — 32-character lowercase hexadecimal string.""" + + +class V1AutomateEventTaskTraceContext(BaseModel): + """Envelope for the "task:trace_context" event from /v1/automate.""" + + data: V1AutomateEventTaskTraceContextData + """Payload for the task:trace_context event. + + Carries the OpenTelemetry trace ID for this /v1/automate request so consumers + can deep-link to distributed-tracing UIs (e.g. Cloud Trace, Cloud Logging) for + the run. + """ + + event: Literal["task:trace_context"] + + class V1AutomateEventTaskValidatedData(BaseModel): """Event data for task validation""" @@ -2630,6 +2567,7 @@ class V1AutomateEventTaskValidationError(BaseModel): V1AutomateEventTaskMetricsIncremental, V1AutomateEventTaskSetup, V1AutomateEventTaskStarted, + V1AutomateEventTaskTraceContext, V1AutomateEventTaskValidated, V1AutomateEventTaskValidationError, ], diff --git a/src/tabstack/types/extract_json_params.py b/src/tabstack/types/extract_json_params.py index d6d27a7..58ea5ff 100644 --- a/src/tabstack/types/extract_json_params.py +++ b/src/tabstack/types/extract_json_params.py @@ -4,7 +4,9 @@ from typing_extensions import Literal, Required, TypedDict -__all__ = ["ExtractJsonParams", "GeoTarget"] +from .shared_params.geotarget_geo_target import GeotargetGeoTarget + +__all__ = ["ExtractJsonParams"] class ExtractJsonParams(TypedDict, total=False): @@ -22,18 +24,8 @@ class ExtractJsonParams(TypedDict, total=False): JS-heavy sites (15-60s). """ - geo_target: GeoTarget + geo_target: GeotargetGeoTarget """Optional geotargeting parameters for proxy requests""" nocache: bool """Bypass cache and force fresh data retrieval""" - - -class GeoTarget(TypedDict, total=False): - """Optional geotargeting parameters for proxy requests""" - - country: str - """ - Country code using ISO 3166-1 alpha-2 standard (2 letters, e.g., "US", "GB", - "JP"). See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - """ diff --git a/src/tabstack/types/extract_markdown_params.py b/src/tabstack/types/extract_markdown_params.py index bd57499..c204227 100644 --- a/src/tabstack/types/extract_markdown_params.py +++ b/src/tabstack/types/extract_markdown_params.py @@ -4,7 +4,9 @@ from typing_extensions import Literal, Required, TypedDict -__all__ = ["ExtractMarkdownParams", "GeoTarget"] +from .shared_params.geotarget_geo_target import GeotargetGeoTarget + +__all__ = ["ExtractMarkdownParams"] class ExtractMarkdownParams(TypedDict, total=False): @@ -19,7 +21,7 @@ class ExtractMarkdownParams(TypedDict, total=False): JS-heavy sites (15-60s). """ - geo_target: GeoTarget + geo_target: GeotargetGeoTarget """Optional geotargeting parameters for proxy requests""" metadata: bool @@ -30,13 +32,3 @@ class ExtractMarkdownParams(TypedDict, total=False): nocache: bool """Bypass cache and force fresh data retrieval""" - - -class GeoTarget(TypedDict, total=False): - """Optional geotargeting parameters for proxy requests""" - - country: str - """ - Country code using ISO 3166-1 alpha-2 standard (2 letters, e.g., "US", "GB", - "JP"). See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - """ diff --git a/src/tabstack/types/generate_json_params.py b/src/tabstack/types/generate_json_params.py index 486e3ed..b1cc9b7 100644 --- a/src/tabstack/types/generate_json_params.py +++ b/src/tabstack/types/generate_json_params.py @@ -4,7 +4,9 @@ from typing_extensions import Literal, Required, TypedDict -__all__ = ["GenerateJsonParams", "GeoTarget"] +from .shared_params.geotarget_geo_target import GeotargetGeoTarget + +__all__ = ["GenerateJsonParams"] class GenerateJsonParams(TypedDict, total=False): @@ -25,18 +27,8 @@ class GenerateJsonParams(TypedDict, total=False): JS-heavy sites (15-60s). """ - geo_target: GeoTarget + geo_target: GeotargetGeoTarget """Optional geotargeting parameters for proxy requests""" nocache: bool """Bypass cache and force fresh data retrieval""" - - -class GeoTarget(TypedDict, total=False): - """Optional geotargeting parameters for proxy requests""" - - country: str - """ - Country code using ISO 3166-1 alpha-2 standard (2 letters, e.g., "US", "GB", - "JP"). See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 - """ diff --git a/src/tabstack/types/research_event.py b/src/tabstack/types/research_event.py index a41f2e9..67ad1dd 100644 --- a/src/tabstack/types/research_event.py +++ b/src/tabstack/types/research_event.py @@ -7,6 +7,7 @@ from .._utils import PropertyInfo from .._models import BaseModel +from .v1_research_question_assessment import V1ResearchQuestionAssessment __all__ = [ "ResearchEvent", @@ -20,7 +21,6 @@ "V1ResearchEventCompleteDataMetadata", "V1ResearchEventCompleteDataMetadataCitedPage", "V1ResearchEventCompleteDataMetadataGapEvaluation", - "V1ResearchEventCompleteDataMetadataGapEvaluationQuestionAssessment", "V1ResearchEventCompleteDataMetadataJudgment", "V1ResearchEventCompleteDataMetadataMetrics", "V1ResearchEventCompleteDataMetadataMetricsPhases", @@ -33,7 +33,6 @@ "V1ResearchEventErrorDataError", "V1ResearchEventEvaluatingEnd", "V1ResearchEventEvaluatingEndData", - "V1ResearchEventEvaluatingEndDataQuestionAssessment", "V1ResearchEventEvaluatingStart", "V1ResearchEventEvaluatingStartData", "V1ResearchEventFollowingEnd", @@ -170,22 +169,6 @@ class V1ResearchEventCompleteDataMetadataCitedPage(BaseModel): """URL source tracking - where a URL came from""" -class V1ResearchEventCompleteDataMetadataGapEvaluationQuestionAssessment(BaseModel): - """Assessment of a single research question""" - - findings: str - """What we learned (if answered/partial) or what's missing (if unanswered)""" - - question: str - """The research question being assessed""" - - status: Literal["answered", "partial", "unanswered"] - """ - Status: answered (clear info), partial (some info, gaps remain), unanswered (no - relevant info) - """ - - class V1ResearchEventCompleteDataMetadataGapEvaluation(BaseModel): """Gap evaluation results from research strategist""" @@ -195,9 +178,7 @@ class V1ResearchEventCompleteDataMetadataGapEvaluation(BaseModel): needed? """ - question_assessments: List[V1ResearchEventCompleteDataMetadataGapEvaluationQuestionAssessment] = FieldInfo( - alias="questionAssessments" - ) + question_assessments: List[V1ResearchQuestionAssessment] = FieldInfo(alias="questionAssessments") """Assessment of each research question's status and findings""" research_coverage: Literal["Light", "Moderate", "Solid", "Comprehensive"] = FieldInfo(alias="researchCoverage") @@ -456,22 +437,6 @@ class V1ResearchEventError(BaseModel): event: Literal["error"] -class V1ResearchEventEvaluatingEndDataQuestionAssessment(BaseModel): - """Assessment of a single research question""" - - findings: str - """What we learned (if answered/partial) or what's missing (if unanswered)""" - - question: str - """The research question being assessed""" - - status: Literal["answered", "partial", "unanswered"] - """ - Status: answered (clear info), partial (some info, gaps remain), unanswered (no - relevant info) - """ - - class V1ResearchEventEvaluatingEndData(BaseModel): coverage: Literal["Light", "Moderate", "Solid", "Comprehensive"] @@ -483,9 +448,7 @@ class V1ResearchEventEvaluatingEndData(BaseModel): next_queries: List[str] = FieldInfo(alias="nextQueries") - question_assessments: List[V1ResearchEventEvaluatingEndDataQuestionAssessment] = FieldInfo( - alias="questionAssessments" - ) + question_assessments: List[V1ResearchQuestionAssessment] = FieldInfo(alias="questionAssessments") should_continue: bool = FieldInfo(alias="shouldContinue") diff --git a/src/tabstack/types/shared/__init__.py b/src/tabstack/types/shared/__init__.py new file mode 100644 index 0000000..797ab9e --- /dev/null +++ b/src/tabstack/types/shared/__init__.py @@ -0,0 +1,3 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .geotarget_geo_target import GeotargetGeoTarget as GeotargetGeoTarget diff --git a/src/tabstack/types/shared/geotarget_geo_target.py b/src/tabstack/types/shared/geotarget_geo_target.py new file mode 100644 index 0000000..88c2cd2 --- /dev/null +++ b/src/tabstack/types/shared/geotarget_geo_target.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional + +from ..._models import BaseModel + +__all__ = ["GeotargetGeoTarget"] + + +class GeotargetGeoTarget(BaseModel): + country: Optional[str] = None + """ + Country code using ISO 3166-1 alpha-2 standard (2 letters, e.g., "US", "GB", + "JP"). See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + """ diff --git a/src/tabstack/types/shared_params/__init__.py b/src/tabstack/types/shared_params/__init__.py new file mode 100644 index 0000000..797ab9e --- /dev/null +++ b/src/tabstack/types/shared_params/__init__.py @@ -0,0 +1,3 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from .geotarget_geo_target import GeotargetGeoTarget as GeotargetGeoTarget diff --git a/src/tabstack/types/shared_params/geotarget_geo_target.py b/src/tabstack/types/shared_params/geotarget_geo_target.py new file mode 100644 index 0000000..204298c --- /dev/null +++ b/src/tabstack/types/shared_params/geotarget_geo_target.py @@ -0,0 +1,15 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import TypedDict + +__all__ = ["GeotargetGeoTarget"] + + +class GeotargetGeoTarget(TypedDict, total=False): + country: str + """ + Country code using ISO 3166-1 alpha-2 standard (2 letters, e.g., "US", "GB", + "JP"). See: https://en.wikipedia.org/wiki/ISO_3166-1_alpha-2 + """ diff --git a/src/tabstack/types/v1_global_buffer.py b/src/tabstack/types/v1_global_buffer.py new file mode 100644 index 0000000..3f48d48 --- /dev/null +++ b/src/tabstack/types/v1_global_buffer.py @@ -0,0 +1,37 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import TYPE_CHECKING, Dict + +from pydantic import Field as FieldInfo + +from .._models import BaseModel + +__all__ = ["V1GlobalBuffer", "Buffer"] + + +class Buffer(BaseModel): + byte_length: float = FieldInfo(alias="byteLength") + + +class V1GlobalBuffer(BaseModel): + buffer: Buffer + + byte_length: float = FieldInfo(alias="byteLength") + + byte_offset: float = FieldInfo(alias="byteOffset") + + bytes_per_element: float = FieldInfo(alias="BYTES_PER_ELEMENT") + + length: float + + if TYPE_CHECKING: + # Some versions of Pydantic <2.8.0 have a bug and don’t allow assigning a + # value to this field, so for compatibility we avoid doing it at runtime. + __pydantic_extra__: Dict[str, float] = FieldInfo(init=False) # pyright: ignore[reportIncompatibleVariableOverride] + + # Stub to indicate that arbitrary properties are accepted. + # To access properties that are not valid identifiers you can use `getattr`, e.g. + # `getattr(obj, '$type')` + def __getattr__(self, attr: str) -> float: ... + else: + __pydantic_extra__: Dict[str, float] diff --git a/src/tabstack/types/v1_research_question_assessment.py b/src/tabstack/types/v1_research_question_assessment.py new file mode 100644 index 0000000..22b7041 --- /dev/null +++ b/src/tabstack/types/v1_research_question_assessment.py @@ -0,0 +1,23 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["V1ResearchQuestionAssessment"] + + +class V1ResearchQuestionAssessment(BaseModel): + """Assessment of a single research question""" + + findings: str + """What we learned (if answered/partial) or what's missing (if unanswered)""" + + question: str + """The research question being assessed""" + + status: Literal["answered", "partial", "unanswered"] + """ + Status: answered (clear info), partial (some info, gaps remain), unanswered (no + relevant info) + """ diff --git a/tests/api_resources/test_extract.py b/tests/api_resources/test_extract.py index 262bac9..e6f463b 100644 --- a/tests/api_resources/test_extract.py +++ b/tests/api_resources/test_extract.py @@ -9,7 +9,10 @@ from tabstack import Tabstack, AsyncTabstack from tests.utils import assert_matches_type -from tabstack.types import ExtractJsonResponse, ExtractMarkdownResponse +from tabstack.types import ( + ExtractJsonResponse, + ExtractMarkdownResponse, +) base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010") diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py index a11b04c..e40b192 100644 --- a/tests/test_extract_files.py +++ b/tests/test_extract_files.py @@ -4,7 +4,7 @@ import pytest -from tabstack._types import FileTypes +from tabstack._types import FileTypes, ArrayFormat from tabstack._utils import extract_files @@ -37,10 +37,7 @@ def test_multiple_files() -> None: def test_top_level_file_array() -> None: query = {"files": [b"file one", b"file two"], "title": "hello"} - assert extract_files(query, paths=[["files", ""]]) == [ - ("files[]", b"file one"), - ("files[]", b"file two"), - ] + assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")] assert query == {"title": "hello"} @@ -71,3 +68,24 @@ def test_ignores_incorrect_paths( expected: list[tuple[str, FileTypes]], ) -> None: assert extract_files(query, paths=paths) == expected + + +@pytest.mark.parametrize( + "array_format,expected_top_level,expected_nested", + [ + ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]), + ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]), + ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]), + ], +) +def test_array_format_controls_file_field_names( + array_format: ArrayFormat, + expected_top_level: list[tuple[str, FileTypes]], + expected_nested: list[tuple[str, FileTypes]], +) -> None: + top_level = {"files": [b"a", b"b"]} + assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level + + nested = {"items": [{"file": b"a"}, {"file": b"b"}]} + assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested diff --git a/tests/test_files.py b/tests/test_files.py index cd25fef..cf7fb6e 100644 --- a/tests/test_files.py +++ b/tests/test_files.py @@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None: copied = deepcopy_with_paths(original, [["items", "", "file"]]) extracted = extract_files(copied, paths=[["items", "", "file"]]) - assert extracted == [("items[][file]", file1), ("items[][file]", file2)] + assert [entry for _, entry in extracted] == [file1, file2] assert original == { "items": [ {"file": file1, "extra": 1},