From 6ef41432e3e2e84354a4a4af059256ed3358ff4b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 19:25:59 +0000 Subject: [PATCH 01/12] codegen metadata --- .stats.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.stats.yml b/.stats.yml index 026a0e1..ae972ff 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-5aeb0321dfa491e03f95682879119e6fe62f3777f7026c85b0fd84ffbcfe957c.yml openapi_spec_hash: 2cdab5faacc1cb28545a9faf4459b629 -config_hash: 1bc6137228160bbee20af307fae135e5 +config_hash: 71c10599b4847b7bccbc56016586d07f From 9197068bb9bd06db9e54a49690e25e9c0f93b230 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:06:23 +0000 Subject: [PATCH 02/12] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-pypi.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index edf03df..1004be1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -19,7 +19,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/tabstack-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v5 @@ -41,7 +41,7 @@ jobs: id-token: write runs-on: ${{ github.repository == 'stainless-sdks/tabstack-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v5 @@ -75,7 +75,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/tabstack-python' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 71c2781..3c25ad0 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -17,7 +17,7 @@ jobs: id-token: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Install uv uses: astral-sh/setup-uv@v5 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index 16b7f32..d3b2e14 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'Mozilla-Ocho/tabstack-python' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | From 6eb4a9feb9f79e9580d5a521de46bb07f23db928 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 19:01:20 +0000 Subject: [PATCH 03/12] feat(api): api update --- .stats.yml | 4 +-- README.md | 16 ++++++++++ src/tabstack/resources/agent.py | 32 ++++++++++++++----- src/tabstack/resources/extract.py | 16 ++++++++++ src/tabstack/resources/generate.py | 8 +++++ src/tabstack/types/agent_automate_params.py | 15 ++++++++- src/tabstack/types/extract_json_params.py | 15 ++++++++- src/tabstack/types/extract_markdown_params.py | 15 ++++++++- src/tabstack/types/generate_json_params.py | 15 ++++++++- tests/api_resources/test_agent.py | 2 ++ tests/api_resources/test_extract.py | 4 +++ tests/api_resources/test_generate.py | 2 ++ 12 files changed, 130 insertions(+), 14 deletions(-) diff --git a/.stats.yml b/.stats.yml index ae972ff..fc635d5 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-5aeb0321dfa491e03f95682879119e6fe62f3777f7026c85b0fd84ffbcfe957c.yml -openapi_spec_hash: 2cdab5faacc1cb28545a9faf4459b629 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-9beea95e241501d17779646d5d17bf0454f03c8d2de5db32b54a97c14320ce6d.yml +openapi_spec_hash: 4bdfce0b12f7ac1b449f870ddd4b1334 config_hash: 71c10599b4847b7bccbc56016586d07f diff --git a/README.md b/README.md index 5d13168..c167de2 100644 --- a/README.md +++ b/README.md @@ -121,6 +121,22 @@ Nested request parameters are [TypedDicts](https://docs.python.org/3/library/typ Typed requests and responses provide autocomplete and documentation within your editor. If you would like to see type errors in VS Code to help catch bugs earlier, set `python.analysis.typeCheckingMode` to `basic`. +## Nested params + +Nested parameters are dictionaries, typed using `TypedDict`, for example: + +```python +from tabstack import Tabstack + +client = Tabstack() + +automate_event = client.agent.automate( + task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", + geotarget={}, +) +print(automate_event.geotarget) +``` + ## Handling errors When the library is unable to connect to the API (for example, due to network connection problems or a timeout), a subclass of `tabstack.APIConnectionError` is raised. diff --git a/src/tabstack/resources/agent.py b/src/tabstack/resources/agent.py index 72856e1..de39af2 100644 --- a/src/tabstack/resources/agent.py +++ b/src/tabstack/resources/agent.py @@ -47,6 +47,7 @@ def automate( *, task: str, data: object | Omit = omit, + geotarget: agent_automate_params.Geotarget | Omit = omit, guardrails: str | Omit = omit, max_iterations: int | Omit = omit, max_validation_attempts: int | Omit = omit, @@ -58,16 +59,20 @@ def automate( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> Stream[AutomateEvent]: - """Execute AI-powered browser automation tasks using natural language. - - This - endpoint **always streams** responses using Server-Sent Events (SSE). + """ + Execute AI-powered browser automation tasks using natural language with optional + geotargeting. This endpoint **always streams** responses using Server-Sent + Events (SSE). **Streaming Response:** - All responses are streamed using Server-Sent Events (`text/event-stream`) - Real-time progress updates and results as they're generated + **Geotargeting:** + + - Optionally specify a country code for geotargeted browsing + **Use Cases:** - Web scraping and data extraction @@ -81,6 +86,8 @@ def automate( data: JSON data to provide context for form filling or complex tasks + geotarget: Optional geotargeting parameters for proxy requests + guardrails: Safety constraints for execution max_iterations: Maximum task iterations @@ -104,6 +111,7 @@ def automate( { "task": task, "data": data, + "geotarget": geotarget, "guardrails": guardrails, "max_iterations": max_iterations, "max_validation_attempts": max_validation_attempts, @@ -145,6 +153,7 @@ async def automate( *, task: str, data: object | Omit = omit, + geotarget: agent_automate_params.Geotarget | Omit = omit, guardrails: str | Omit = omit, max_iterations: int | Omit = omit, max_validation_attempts: int | Omit = omit, @@ -156,16 +165,20 @@ async def automate( extra_body: Body | None = None, timeout: float | httpx.Timeout | None | NotGiven = not_given, ) -> AsyncStream[AutomateEvent]: - """Execute AI-powered browser automation tasks using natural language. - - This - endpoint **always streams** responses using Server-Sent Events (SSE). + """ + Execute AI-powered browser automation tasks using natural language with optional + geotargeting. This endpoint **always streams** responses using Server-Sent + Events (SSE). **Streaming Response:** - All responses are streamed using Server-Sent Events (`text/event-stream`) - Real-time progress updates and results as they're generated + **Geotargeting:** + + - Optionally specify a country code for geotargeted browsing + **Use Cases:** - Web scraping and data extraction @@ -179,6 +192,8 @@ async def automate( data: JSON data to provide context for form filling or complex tasks + geotarget: Optional geotargeting parameters for proxy requests + guardrails: Safety constraints for execution max_iterations: Maximum task iterations @@ -202,6 +217,7 @@ async def automate( { "task": task, "data": data, + "geotarget": geotarget, "guardrails": guardrails, "max_iterations": max_iterations, "max_validation_attempts": max_validation_attempts, diff --git a/src/tabstack/resources/extract.py b/src/tabstack/resources/extract.py index 2805fd6..bfff271 100644 --- a/src/tabstack/resources/extract.py +++ b/src/tabstack/resources/extract.py @@ -47,6 +47,7 @@ def json( *, json_schema: object, url: str, + geotarget: extract_json_params.Geotarget | 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. @@ -63,6 +64,8 @@ def json( url: URL to fetch and extract data from + geotarget: Optional geotargeting parameters for proxy requests + nocache: Bypass cache and force fresh data retrieval extra_headers: Send extra headers @@ -79,6 +82,7 @@ def json( { "json_schema": json_schema, "url": url, + "geotarget": geotarget, "nocache": nocache, }, extract_json_params.ExtractJsonParams, @@ -93,6 +97,7 @@ def markdown( self, *, url: str, + geotarget: extract_markdown_params.Geotarget | 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. @@ -109,6 +114,8 @@ def markdown( Args: url: URL to fetch and convert to markdown + geotarget: Optional geotargeting parameters for proxy requests + metadata: Include extracted metadata (Open Graph and HTML metadata) as a separate field in the response @@ -127,6 +134,7 @@ def markdown( body=maybe_transform( { "url": url, + "geotarget": geotarget, "metadata": metadata, "nocache": nocache, }, @@ -164,6 +172,7 @@ async def json( *, json_schema: object, url: str, + geotarget: extract_json_params.Geotarget | 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. @@ -180,6 +189,8 @@ async def json( url: URL to fetch and extract data from + geotarget: Optional geotargeting parameters for proxy requests + nocache: Bypass cache and force fresh data retrieval extra_headers: Send extra headers @@ -196,6 +207,7 @@ async def json( { "json_schema": json_schema, "url": url, + "geotarget": geotarget, "nocache": nocache, }, extract_json_params.ExtractJsonParams, @@ -210,6 +222,7 @@ async def markdown( self, *, url: str, + geotarget: extract_markdown_params.Geotarget | 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. @@ -226,6 +239,8 @@ async def markdown( Args: url: URL to fetch and convert to markdown + geotarget: Optional geotargeting parameters for proxy requests + metadata: Include extracted metadata (Open Graph and HTML metadata) as a separate field in the response @@ -244,6 +259,7 @@ async def markdown( body=await async_maybe_transform( { "url": url, + "geotarget": geotarget, "metadata": metadata, "nocache": nocache, }, diff --git a/src/tabstack/resources/generate.py b/src/tabstack/resources/generate.py index 36d0c4d..65c86dd 100644 --- a/src/tabstack/resources/generate.py +++ b/src/tabstack/resources/generate.py @@ -47,6 +47,7 @@ def json( instructions: str, json_schema: object, url: str, + geotarget: generate_json_params.Geotarget | 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. @@ -66,6 +67,8 @@ def json( url: URL to fetch content from + geotarget: Optional geotargeting parameters for proxy requests + nocache: Bypass cache and force fresh data retrieval extra_headers: Send extra headers @@ -83,6 +86,7 @@ def json( "instructions": instructions, "json_schema": json_schema, "url": url, + "geotarget": geotarget, "nocache": nocache, }, generate_json_params.GenerateJsonParams, @@ -120,6 +124,7 @@ async def json( instructions: str, json_schema: object, url: str, + geotarget: generate_json_params.Geotarget | 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. @@ -139,6 +144,8 @@ async def json( url: URL to fetch content from + geotarget: Optional geotargeting parameters for proxy requests + nocache: Bypass cache and force fresh data retrieval extra_headers: Send extra headers @@ -156,6 +163,7 @@ async def json( "instructions": instructions, "json_schema": json_schema, "url": url, + "geotarget": geotarget, "nocache": nocache, }, generate_json_params.GenerateJsonParams, diff --git a/src/tabstack/types/agent_automate_params.py b/src/tabstack/types/agent_automate_params.py index e36d15b..96803d1 100644 --- a/src/tabstack/types/agent_automate_params.py +++ b/src/tabstack/types/agent_automate_params.py @@ -6,7 +6,7 @@ from .._utils import PropertyInfo -__all__ = ["AgentAutomateParams"] +__all__ = ["AgentAutomateParams", "Geotarget"] class AgentAutomateParams(TypedDict, total=False): @@ -16,6 +16,9 @@ class AgentAutomateParams(TypedDict, total=False): data: object """JSON data to provide context for form filling or complex tasks""" + geotarget: Geotarget + """Optional geotargeting parameters for proxy requests""" + guardrails: str """Safety constraints for execution""" @@ -27,3 +30,13 @@ 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/extract_json_params.py b/src/tabstack/types/extract_json_params.py index 58aa6b0..eeb1bba 100644 --- a/src/tabstack/types/extract_json_params.py +++ b/src/tabstack/types/extract_json_params.py @@ -4,7 +4,7 @@ from typing_extensions import Required, TypedDict -__all__ = ["ExtractJsonParams"] +__all__ = ["ExtractJsonParams", "Geotarget"] class ExtractJsonParams(TypedDict, total=False): @@ -14,5 +14,18 @@ class ExtractJsonParams(TypedDict, total=False): url: Required[str] """URL to fetch and extract data from""" + geotarget: Geotarget + """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 f5519dc..e0dbea2 100644 --- a/src/tabstack/types/extract_markdown_params.py +++ b/src/tabstack/types/extract_markdown_params.py @@ -4,13 +4,16 @@ from typing_extensions import Required, TypedDict -__all__ = ["ExtractMarkdownParams"] +__all__ = ["ExtractMarkdownParams", "Geotarget"] class ExtractMarkdownParams(TypedDict, total=False): url: Required[str] """URL to fetch and convert to markdown""" + geotarget: Geotarget + """Optional geotargeting parameters for proxy requests""" + metadata: bool """ Include extracted metadata (Open Graph and HTML metadata) as a separate field in @@ -19,3 +22,13 @@ 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 3158b97..ab375fc 100644 --- a/src/tabstack/types/generate_json_params.py +++ b/src/tabstack/types/generate_json_params.py @@ -4,7 +4,7 @@ from typing_extensions import Required, TypedDict -__all__ = ["GenerateJsonParams"] +__all__ = ["GenerateJsonParams", "Geotarget"] class GenerateJsonParams(TypedDict, total=False): @@ -17,5 +17,18 @@ class GenerateJsonParams(TypedDict, total=False): url: Required[str] """URL to fetch content from""" + geotarget: Geotarget + """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/tests/api_resources/test_agent.py b/tests/api_resources/test_agent.py index 9cc9f8e..4aa4719 100644 --- a/tests/api_resources/test_agent.py +++ b/tests/api_resources/test_agent.py @@ -29,6 +29,7 @@ def test_method_automate_with_all_params(self, client: Tabstack) -> None: agent_stream = client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", data={}, + geotarget={"country": "US"}, guardrails="browse and extract only, don't interact with repositories", max_iterations=50, max_validation_attempts=3, @@ -81,6 +82,7 @@ async def test_method_automate_with_all_params(self, async_client: AsyncTabstack agent_stream = await async_client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", data={}, + geotarget={"country": "US"}, guardrails="browse and extract only, don't interact with repositories", max_iterations=50, max_validation_attempts=3, diff --git a/tests/api_resources/test_extract.py b/tests/api_resources/test_extract.py index 4590a4a..e17a0a2 100644 --- a/tests/api_resources/test_extract.py +++ b/tests/api_resources/test_extract.py @@ -32,6 +32,7 @@ def test_method_json_with_all_params(self, client: Tabstack) -> None: extract = client.extract.json( json_schema={}, url="https://news.ycombinator.com", + geotarget={"country": "US"}, nocache=False, ) assert_matches_type(ExtractJsonResponse, extract, path=["response"]) @@ -77,6 +78,7 @@ def test_method_markdown(self, client: Tabstack) -> None: def test_method_markdown_with_all_params(self, client: Tabstack) -> None: extract = client.extract.markdown( url="https://example.com/blog/article", + geotarget={"country": "US"}, metadata=True, nocache=False, ) @@ -129,6 +131,7 @@ async def test_method_json_with_all_params(self, async_client: AsyncTabstack) -> extract = await async_client.extract.json( json_schema={}, url="https://news.ycombinator.com", + geotarget={"country": "US"}, nocache=False, ) assert_matches_type(ExtractJsonResponse, extract, path=["response"]) @@ -174,6 +177,7 @@ async def test_method_markdown(self, async_client: AsyncTabstack) -> None: async def test_method_markdown_with_all_params(self, async_client: AsyncTabstack) -> None: extract = await async_client.extract.markdown( url="https://example.com/blog/article", + geotarget={"country": "US"}, metadata=True, nocache=False, ) diff --git a/tests/api_resources/test_generate.py b/tests/api_resources/test_generate.py index 7e1e167..0251aba 100644 --- a/tests/api_resources/test_generate.py +++ b/tests/api_resources/test_generate.py @@ -34,6 +34,7 @@ def test_method_json_with_all_params(self, client: Tabstack) -> None: instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", json_schema={}, url="https://news.ycombinator.com", + geotarget={"country": "US"}, nocache=False, ) assert_matches_type(GenerateJsonResponse, generate, path=["response"]) @@ -91,6 +92,7 @@ async def test_method_json_with_all_params(self, async_client: AsyncTabstack) -> instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", json_schema={}, url="https://news.ycombinator.com", + geotarget={"country": "US"}, nocache=False, ) assert_matches_type(GenerateJsonResponse, generate, path=["response"]) From b8a1e095bd6153948c18a3b15324fd0166cf136c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 22:22:34 +0000 Subject: [PATCH 04/12] feat(api): api update --- .stats.yml | 4 ++-- README.md | 4 ++-- src/tabstack/resources/agent.py | 12 +++++----- src/tabstack/resources/extract.py | 24 +++++++++---------- src/tabstack/resources/generate.py | 12 +++++----- src/tabstack/types/agent_automate_params.py | 6 ++--- src/tabstack/types/extract_json_params.py | 6 ++--- src/tabstack/types/extract_markdown_params.py | 6 ++--- src/tabstack/types/generate_json_params.py | 6 ++--- tests/api_resources/test_agent.py | 4 ++-- tests/api_resources/test_extract.py | 8 +++---- tests/api_resources/test_generate.py | 4 ++-- 12 files changed, 48 insertions(+), 48 deletions(-) diff --git a/.stats.yml b/.stats.yml index fc635d5..ac00624 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-9beea95e241501d17779646d5d17bf0454f03c8d2de5db32b54a97c14320ce6d.yml -openapi_spec_hash: 4bdfce0b12f7ac1b449f870ddd4b1334 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-f2c88dc306a938cf16725a067504d53592a3d941536879a3b78dc0fb58cd8d02.yml +openapi_spec_hash: 3f933bb0e9e911694ab9187907de0581 config_hash: 71c10599b4847b7bccbc56016586d07f diff --git a/README.md b/README.md index c167de2..1f29c2d 100644 --- a/README.md +++ b/README.md @@ -132,9 +132,9 @@ client = Tabstack() automate_event = client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", - geotarget={}, + geo_target={}, ) -print(automate_event.geotarget) +print(automate_event.geo_target) ``` ## Handling errors diff --git a/src/tabstack/resources/agent.py b/src/tabstack/resources/agent.py index de39af2..e303de4 100644 --- a/src/tabstack/resources/agent.py +++ b/src/tabstack/resources/agent.py @@ -47,7 +47,7 @@ def automate( *, task: str, data: object | Omit = omit, - geotarget: agent_automate_params.Geotarget | Omit = omit, + geo_target: agent_automate_params.GeoTarget | Omit = omit, guardrails: str | Omit = omit, max_iterations: int | Omit = omit, max_validation_attempts: int | Omit = omit, @@ -86,7 +86,7 @@ def automate( data: JSON data to provide context for form filling or complex tasks - geotarget: Optional geotargeting parameters for proxy requests + geo_target: Optional geotargeting parameters for proxy requests guardrails: Safety constraints for execution @@ -111,7 +111,7 @@ def automate( { "task": task, "data": data, - "geotarget": geotarget, + "geo_target": geo_target, "guardrails": guardrails, "max_iterations": max_iterations, "max_validation_attempts": max_validation_attempts, @@ -153,7 +153,7 @@ async def automate( *, task: str, data: object | Omit = omit, - geotarget: agent_automate_params.Geotarget | Omit = omit, + geo_target: agent_automate_params.GeoTarget | Omit = omit, guardrails: str | Omit = omit, max_iterations: int | Omit = omit, max_validation_attempts: int | Omit = omit, @@ -192,7 +192,7 @@ async def automate( data: JSON data to provide context for form filling or complex tasks - geotarget: Optional geotargeting parameters for proxy requests + geo_target: Optional geotargeting parameters for proxy requests guardrails: Safety constraints for execution @@ -217,7 +217,7 @@ async def automate( { "task": task, "data": data, - "geotarget": geotarget, + "geo_target": geo_target, "guardrails": guardrails, "max_iterations": max_iterations, "max_validation_attempts": max_validation_attempts, diff --git a/src/tabstack/resources/extract.py b/src/tabstack/resources/extract.py index bfff271..20984a8 100644 --- a/src/tabstack/resources/extract.py +++ b/src/tabstack/resources/extract.py @@ -47,7 +47,7 @@ def json( *, json_schema: object, url: str, - geotarget: extract_json_params.Geotarget | Omit = omit, + geo_target: extract_json_params.GeoTarget | 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. @@ -64,7 +64,7 @@ def json( url: URL to fetch and extract data from - geotarget: Optional geotargeting parameters for proxy requests + geo_target: Optional geotargeting parameters for proxy requests nocache: Bypass cache and force fresh data retrieval @@ -82,7 +82,7 @@ def json( { "json_schema": json_schema, "url": url, - "geotarget": geotarget, + "geo_target": geo_target, "nocache": nocache, }, extract_json_params.ExtractJsonParams, @@ -97,7 +97,7 @@ def markdown( self, *, url: str, - geotarget: extract_markdown_params.Geotarget | Omit = omit, + geo_target: extract_markdown_params.GeoTarget | 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. @@ -114,7 +114,7 @@ def markdown( Args: url: URL to fetch and convert to markdown - geotarget: Optional geotargeting parameters for proxy requests + geo_target: Optional geotargeting parameters for proxy requests metadata: Include extracted metadata (Open Graph and HTML metadata) as a separate field in the response @@ -134,7 +134,7 @@ def markdown( body=maybe_transform( { "url": url, - "geotarget": geotarget, + "geo_target": geo_target, "metadata": metadata, "nocache": nocache, }, @@ -172,7 +172,7 @@ async def json( *, json_schema: object, url: str, - geotarget: extract_json_params.Geotarget | Omit = omit, + geo_target: extract_json_params.GeoTarget | 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. @@ -189,7 +189,7 @@ async def json( url: URL to fetch and extract data from - geotarget: Optional geotargeting parameters for proxy requests + geo_target: Optional geotargeting parameters for proxy requests nocache: Bypass cache and force fresh data retrieval @@ -207,7 +207,7 @@ async def json( { "json_schema": json_schema, "url": url, - "geotarget": geotarget, + "geo_target": geo_target, "nocache": nocache, }, extract_json_params.ExtractJsonParams, @@ -222,7 +222,7 @@ async def markdown( self, *, url: str, - geotarget: extract_markdown_params.Geotarget | Omit = omit, + geo_target: extract_markdown_params.GeoTarget | 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. @@ -239,7 +239,7 @@ async def markdown( Args: url: URL to fetch and convert to markdown - geotarget: Optional geotargeting parameters for proxy requests + geo_target: Optional geotargeting parameters for proxy requests metadata: Include extracted metadata (Open Graph and HTML metadata) as a separate field in the response @@ -259,7 +259,7 @@ async def markdown( body=await async_maybe_transform( { "url": url, - "geotarget": geotarget, + "geo_target": geo_target, "metadata": metadata, "nocache": nocache, }, diff --git a/src/tabstack/resources/generate.py b/src/tabstack/resources/generate.py index 65c86dd..49aabd2 100644 --- a/src/tabstack/resources/generate.py +++ b/src/tabstack/resources/generate.py @@ -47,7 +47,7 @@ def json( instructions: str, json_schema: object, url: str, - geotarget: generate_json_params.Geotarget | Omit = omit, + geo_target: generate_json_params.GeoTarget | 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. @@ -67,7 +67,7 @@ def json( url: URL to fetch content from - geotarget: Optional geotargeting parameters for proxy requests + geo_target: Optional geotargeting parameters for proxy requests nocache: Bypass cache and force fresh data retrieval @@ -86,7 +86,7 @@ def json( "instructions": instructions, "json_schema": json_schema, "url": url, - "geotarget": geotarget, + "geo_target": geo_target, "nocache": nocache, }, generate_json_params.GenerateJsonParams, @@ -124,7 +124,7 @@ async def json( instructions: str, json_schema: object, url: str, - geotarget: generate_json_params.Geotarget | Omit = omit, + geo_target: generate_json_params.GeoTarget | 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. @@ -144,7 +144,7 @@ async def json( url: URL to fetch content from - geotarget: Optional geotargeting parameters for proxy requests + geo_target: Optional geotargeting parameters for proxy requests nocache: Bypass cache and force fresh data retrieval @@ -163,7 +163,7 @@ async def json( "instructions": instructions, "json_schema": json_schema, "url": url, - "geotarget": geotarget, + "geo_target": geo_target, "nocache": nocache, }, generate_json_params.GenerateJsonParams, diff --git a/src/tabstack/types/agent_automate_params.py b/src/tabstack/types/agent_automate_params.py index 96803d1..350bb30 100644 --- a/src/tabstack/types/agent_automate_params.py +++ b/src/tabstack/types/agent_automate_params.py @@ -6,7 +6,7 @@ from .._utils import PropertyInfo -__all__ = ["AgentAutomateParams", "Geotarget"] +__all__ = ["AgentAutomateParams", "GeoTarget"] class AgentAutomateParams(TypedDict, total=False): @@ -16,7 +16,7 @@ class AgentAutomateParams(TypedDict, total=False): data: object """JSON data to provide context for form filling or complex tasks""" - geotarget: Geotarget + geo_target: GeoTarget """Optional geotargeting parameters for proxy requests""" guardrails: str @@ -32,7 +32,7 @@ class AgentAutomateParams(TypedDict, total=False): """Starting URL for the task""" -class Geotarget(TypedDict, total=False): +class GeoTarget(TypedDict, total=False): """Optional geotargeting parameters for proxy requests""" country: str diff --git a/src/tabstack/types/extract_json_params.py b/src/tabstack/types/extract_json_params.py index eeb1bba..8331e16 100644 --- a/src/tabstack/types/extract_json_params.py +++ b/src/tabstack/types/extract_json_params.py @@ -4,7 +4,7 @@ from typing_extensions import Required, TypedDict -__all__ = ["ExtractJsonParams", "Geotarget"] +__all__ = ["ExtractJsonParams", "GeoTarget"] class ExtractJsonParams(TypedDict, total=False): @@ -14,14 +14,14 @@ class ExtractJsonParams(TypedDict, total=False): url: Required[str] """URL to fetch and extract data from""" - geotarget: Geotarget + geo_target: GeoTarget """Optional geotargeting parameters for proxy requests""" nocache: bool """Bypass cache and force fresh data retrieval""" -class Geotarget(TypedDict, total=False): +class GeoTarget(TypedDict, total=False): """Optional geotargeting parameters for proxy requests""" country: str diff --git a/src/tabstack/types/extract_markdown_params.py b/src/tabstack/types/extract_markdown_params.py index e0dbea2..a13dc4d 100644 --- a/src/tabstack/types/extract_markdown_params.py +++ b/src/tabstack/types/extract_markdown_params.py @@ -4,14 +4,14 @@ from typing_extensions import Required, TypedDict -__all__ = ["ExtractMarkdownParams", "Geotarget"] +__all__ = ["ExtractMarkdownParams", "GeoTarget"] class ExtractMarkdownParams(TypedDict, total=False): url: Required[str] """URL to fetch and convert to markdown""" - geotarget: Geotarget + geo_target: GeoTarget """Optional geotargeting parameters for proxy requests""" metadata: bool @@ -24,7 +24,7 @@ class ExtractMarkdownParams(TypedDict, total=False): """Bypass cache and force fresh data retrieval""" -class Geotarget(TypedDict, total=False): +class GeoTarget(TypedDict, total=False): """Optional geotargeting parameters for proxy requests""" country: str diff --git a/src/tabstack/types/generate_json_params.py b/src/tabstack/types/generate_json_params.py index ab375fc..33cb829 100644 --- a/src/tabstack/types/generate_json_params.py +++ b/src/tabstack/types/generate_json_params.py @@ -4,7 +4,7 @@ from typing_extensions import Required, TypedDict -__all__ = ["GenerateJsonParams", "Geotarget"] +__all__ = ["GenerateJsonParams", "GeoTarget"] class GenerateJsonParams(TypedDict, total=False): @@ -17,14 +17,14 @@ class GenerateJsonParams(TypedDict, total=False): url: Required[str] """URL to fetch content from""" - geotarget: Geotarget + geo_target: GeoTarget """Optional geotargeting parameters for proxy requests""" nocache: bool """Bypass cache and force fresh data retrieval""" -class Geotarget(TypedDict, total=False): +class GeoTarget(TypedDict, total=False): """Optional geotargeting parameters for proxy requests""" country: str diff --git a/tests/api_resources/test_agent.py b/tests/api_resources/test_agent.py index 4aa4719..62cb8ee 100644 --- a/tests/api_resources/test_agent.py +++ b/tests/api_resources/test_agent.py @@ -29,7 +29,7 @@ def test_method_automate_with_all_params(self, client: Tabstack) -> None: agent_stream = client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", data={}, - geotarget={"country": "US"}, + geo_target={"country": "US"}, guardrails="browse and extract only, don't interact with repositories", max_iterations=50, max_validation_attempts=3, @@ -82,7 +82,7 @@ async def test_method_automate_with_all_params(self, async_client: AsyncTabstack agent_stream = await async_client.agent.automate( task="Find the top 3 trending repositories and extract their names, descriptions, and star counts", data={}, - geotarget={"country": "US"}, + geo_target={"country": "US"}, guardrails="browse and extract only, don't interact with repositories", max_iterations=50, max_validation_attempts=3, diff --git a/tests/api_resources/test_extract.py b/tests/api_resources/test_extract.py index e17a0a2..a41439c 100644 --- a/tests/api_resources/test_extract.py +++ b/tests/api_resources/test_extract.py @@ -32,7 +32,7 @@ def test_method_json_with_all_params(self, client: Tabstack) -> None: extract = client.extract.json( json_schema={}, url="https://news.ycombinator.com", - geotarget={"country": "US"}, + geo_target={"country": "US"}, nocache=False, ) assert_matches_type(ExtractJsonResponse, extract, path=["response"]) @@ -78,7 +78,7 @@ def test_method_markdown(self, client: Tabstack) -> None: def test_method_markdown_with_all_params(self, client: Tabstack) -> None: extract = client.extract.markdown( url="https://example.com/blog/article", - geotarget={"country": "US"}, + geo_target={"country": "US"}, metadata=True, nocache=False, ) @@ -131,7 +131,7 @@ async def test_method_json_with_all_params(self, async_client: AsyncTabstack) -> extract = await async_client.extract.json( json_schema={}, url="https://news.ycombinator.com", - geotarget={"country": "US"}, + geo_target={"country": "US"}, nocache=False, ) assert_matches_type(ExtractJsonResponse, extract, path=["response"]) @@ -177,7 +177,7 @@ async def test_method_markdown(self, async_client: AsyncTabstack) -> None: async def test_method_markdown_with_all_params(self, async_client: AsyncTabstack) -> None: extract = await async_client.extract.markdown( url="https://example.com/blog/article", - geotarget={"country": "US"}, + geo_target={"country": "US"}, metadata=True, nocache=False, ) diff --git a/tests/api_resources/test_generate.py b/tests/api_resources/test_generate.py index 0251aba..eb0c4c6 100644 --- a/tests/api_resources/test_generate.py +++ b/tests/api_resources/test_generate.py @@ -34,7 +34,7 @@ def test_method_json_with_all_params(self, client: Tabstack) -> None: instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", json_schema={}, url="https://news.ycombinator.com", - geotarget={"country": "US"}, + geo_target={"country": "US"}, nocache=False, ) assert_matches_type(GenerateJsonResponse, generate, path=["response"]) @@ -92,7 +92,7 @@ async def test_method_json_with_all_params(self, async_client: AsyncTabstack) -> instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", json_schema={}, url="https://news.ycombinator.com", - geotarget={"country": "US"}, + geo_target={"country": "US"}, nocache=False, ) assert_matches_type(GenerateJsonResponse, generate, path=["response"]) From b6d62f664f7cabaa8201fcb02fe50d307e82553c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 26 Jan 2026 12:57:04 +0000 Subject: [PATCH 05/12] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1004be1..051a17f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -57,7 +57,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/tabstack-python' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From ac0c74625af51293f79dbca80540781a4589adab Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 27 Jan 2026 00:04:07 +0000 Subject: [PATCH 06/12] feat(api): api update --- .stats.yml | 4 +-- .../types/extract_markdown_response.py | 29 ++++++++++++++++++- 2 files changed, 30 insertions(+), 3 deletions(-) diff --git a/.stats.yml b/.stats.yml index ac00624..b07d714 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-f2c88dc306a938cf16725a067504d53592a3d941536879a3b78dc0fb58cd8d02.yml -openapi_spec_hash: 3f933bb0e9e911694ab9187907de0581 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-2452b7acf02a6afbb200a788317469e0ee1038117d6940a9813f8b72835e1ec1.yml +openapi_spec_hash: 2c61a542b0bd70f5b7c7d996c682a0c0 config_hash: 71c10599b4847b7bccbc56016586d07f diff --git a/src/tabstack/types/extract_markdown_response.py b/src/tabstack/types/extract_markdown_response.py index eb51194..2070176 100644 --- a/src/tabstack/types/extract_markdown_response.py +++ b/src/tabstack/types/extract_markdown_response.py @@ -1,6 +1,6 @@ # File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. -from typing import Optional +from typing import List, Optional from .._models import BaseModel @@ -15,18 +15,45 @@ class Metadata(BaseModel): author: Optional[str] = None """Author information from HTML metadata""" + created_at: Optional[str] = None + """Document creation date (ISO 8601)""" + + creator: Optional[str] = None + """Creator application (e.g., "Microsoft Word")""" + description: Optional[str] = None """Page description from Open Graph or HTML""" image: Optional[str] = None """Featured image URL from Open Graph""" + keywords: Optional[List[str]] = None + """PDF keywords as array""" + + modified_at: Optional[str] = None + """Document modification date (ISO 8601)""" + + page_count: Optional[int] = None + """Number of pages (PDF documents)""" + + pdf_version: Optional[str] = None + """PDF version (e.g., "1.5")""" + + producer: Optional[str] = None + """PDF producer software (e.g., "Adobe PDF Library")""" + publisher: Optional[str] = None """Publisher information from Open Graph""" site_name: Optional[str] = None """Site name from Open Graph""" + subject: Optional[str] = None + """ + PDF-specific metadata fields (populated for PDF documents) PDF subject or + summary + """ + title: Optional[str] = None """Page title from Open Graph or HTML""" From c8fba6725a642e10179f6a0006642f5dfe281c3c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 28 Jan 2026 16:41:46 +0000 Subject: [PATCH 07/12] fix(docs): fix mcp installation instructions for remote servers --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1f29c2d..58421ee 100644 --- a/README.md +++ b/README.md @@ -13,8 +13,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Tabstack MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=tabstack-mcp&config=eyJuYW1lIjoidGFic3RhY2stbWNwIiwidHJhbnNwb3J0Ijoic3NlIiwidXJsIjoiaHR0cHM6Ly90YWJzdGFjay5zdGxtY3AuY29tL3NzZSJ9) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22tabstack-mcp%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Ftabstack.stlmcp.com%2Fsse%22%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=tabstack-mcp&config=eyJuYW1lIjoidGFic3RhY2stbWNwIiwidHJhbnNwb3J0IjoiaHR0cCIsInVybCI6Imh0dHBzOi8vdGFic3RhY2suc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC10YWJzdGFjay1hcGkta2V5IjoiTXkgQVBJIEtleSJ9fQ) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22tabstack-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Ftabstack.stlmcp.com%22%2C%22headers%22%3A%7B%22x-tabstack-api-key%22%3A%22My%20API%20Key%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From 4b5ce35a61dcc1e1c60d5d6dbfea22311c663cdb Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 16:55:30 +0000 Subject: [PATCH 08/12] feat(client): add custom JSON encoder for extended type support --- src/tabstack/_base_client.py | 7 +- src/tabstack/_compat.py | 6 +- src/tabstack/_utils/_json.py | 35 ++++++++++ tests/test_utils/test_json.py | 126 ++++++++++++++++++++++++++++++++++ 4 files changed, 169 insertions(+), 5 deletions(-) create mode 100644 src/tabstack/_utils/_json.py create mode 100644 tests/test_utils/test_json.py diff --git a/src/tabstack/_base_client.py b/src/tabstack/_base_client.py index 19ccfd4..8df444d 100644 --- a/src/tabstack/_base_client.py +++ b/src/tabstack/_base_client.py @@ -86,6 +86,7 @@ APIConnectionError, APIResponseValidationError, ) +from ._utils._json import openapi_dumps log: logging.Logger = logging.getLogger(__name__) @@ -554,8 +555,10 @@ def _build_request( kwargs["content"] = options.content elif isinstance(json_data, bytes): kwargs["content"] = json_data - else: - kwargs["json"] = json_data if is_given(json_data) else None + elif not files: + # Don't set content when JSON is sent as multipart/form-data, + # since httpx's content param overrides other body arguments + kwargs["content"] = openapi_dumps(json_data) if is_given(json_data) and json_data is not None else None kwargs["files"] = files else: headers.pop("Content-Type", None) diff --git a/src/tabstack/_compat.py b/src/tabstack/_compat.py index bdef67f..786ff42 100644 --- a/src/tabstack/_compat.py +++ b/src/tabstack/_compat.py @@ -139,6 +139,7 @@ def model_dump( exclude_defaults: bool = False, warnings: bool = True, mode: Literal["json", "python"] = "python", + by_alias: bool | None = None, ) -> dict[str, Any]: if (not PYDANTIC_V1) or hasattr(model, "model_dump"): return model.model_dump( @@ -148,13 +149,12 @@ def model_dump( exclude_defaults=exclude_defaults, # warnings are not supported in Pydantic v1 warnings=True if PYDANTIC_V1 else warnings, + by_alias=by_alias, ) return cast( "dict[str, Any]", model.dict( # pyright: ignore[reportDeprecated, reportUnnecessaryCast] - exclude=exclude, - exclude_unset=exclude_unset, - exclude_defaults=exclude_defaults, + exclude=exclude, exclude_unset=exclude_unset, exclude_defaults=exclude_defaults, by_alias=bool(by_alias) ), ) diff --git a/src/tabstack/_utils/_json.py b/src/tabstack/_utils/_json.py new file mode 100644 index 0000000..6058421 --- /dev/null +++ b/src/tabstack/_utils/_json.py @@ -0,0 +1,35 @@ +import json +from typing import Any +from datetime import datetime +from typing_extensions import override + +import pydantic + +from .._compat import model_dump + + +def openapi_dumps(obj: Any) -> bytes: + """ + Serialize an object to UTF-8 encoded JSON bytes. + + Extends the standard json.dumps with support for additional types + commonly used in the SDK, such as `datetime`, `pydantic.BaseModel`, etc. + """ + return json.dumps( + obj, + cls=_CustomEncoder, + # Uses the same defaults as httpx's JSON serialization + ensure_ascii=False, + separators=(",", ":"), + allow_nan=False, + ).encode() + + +class _CustomEncoder(json.JSONEncoder): + @override + def default(self, o: Any) -> Any: + if isinstance(o, datetime): + return o.isoformat() + if isinstance(o, pydantic.BaseModel): + return model_dump(o, exclude_unset=True, mode="json", by_alias=True) + return super().default(o) diff --git a/tests/test_utils/test_json.py b/tests/test_utils/test_json.py new file mode 100644 index 0000000..a7ac310 --- /dev/null +++ b/tests/test_utils/test_json.py @@ -0,0 +1,126 @@ +from __future__ import annotations + +import datetime +from typing import Union + +import pydantic + +from tabstack import _compat +from tabstack._utils._json import openapi_dumps + + +class TestOpenapiDumps: + def test_basic(self) -> None: + data = {"key": "value", "number": 42} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"key":"value","number":42}' + + def test_datetime_serialization(self) -> None: + dt = datetime.datetime(2023, 1, 1, 12, 0, 0) + data = {"datetime": dt} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"datetime":"2023-01-01T12:00:00"}' + + def test_pydantic_model_serialization(self) -> None: + class User(pydantic.BaseModel): + first_name: str + last_name: str + age: int + + model_instance = User(first_name="John", last_name="Kramer", age=83) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"first_name":"John","last_name":"Kramer","age":83}}' + + def test_pydantic_model_with_default_values(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + score: int = 0 + + model_instance = User(name="Alice") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Alice"}}' + + def test_pydantic_model_with_default_values_overridden(self) -> None: + class User(pydantic.BaseModel): + name: str + role: str = "user" + active: bool = True + + model_instance = User(name="Bob", role="admin", active=False) + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Bob","role":"admin","active":false}}' + + def test_pydantic_model_with_alias(self) -> None: + class User(pydantic.BaseModel): + first_name: str = pydantic.Field(alias="firstName") + last_name: str = pydantic.Field(alias="lastName") + + model_instance = User(firstName="John", lastName="Doe") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"firstName":"John","lastName":"Doe"}}' + + def test_pydantic_model_with_alias_and_default(self) -> None: + class User(pydantic.BaseModel): + user_name: str = pydantic.Field(alias="userName") + user_role: str = pydantic.Field(default="member", alias="userRole") + is_active: bool = pydantic.Field(default=True, alias="isActive") + + model_instance = User(userName="charlie") + data = {"model": model_instance} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"charlie"}}' + + model_with_overrides = User(userName="diana", userRole="admin", isActive=False) + data = {"model": model_with_overrides} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"userName":"diana","userRole":"admin","isActive":false}}' + + def test_pydantic_model_with_nested_models_and_defaults(self) -> None: + class Address(pydantic.BaseModel): + street: str + city: str = "Unknown" + + class User(pydantic.BaseModel): + name: str + address: Address + verified: bool = False + + if _compat.PYDANTIC_V1: + # to handle forward references in Pydantic v1 + User.update_forward_refs(**locals()) # type: ignore[reportDeprecated] + + address = Address(street="123 Main St") + user = User(name="Diana", address=address) + data = {"user": user} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"user":{"name":"Diana","address":{"street":"123 Main St"}}}' + + address_with_city = Address(street="456 Oak Ave", city="Boston") + user_verified = User(name="Eve", address=address_with_city, verified=True) + data = {"user": user_verified} + json_bytes = openapi_dumps(data) + assert ( + json_bytes == b'{"user":{"name":"Eve","address":{"street":"456 Oak Ave","city":"Boston"},"verified":true}}' + ) + + def test_pydantic_model_with_optional_fields(self) -> None: + class User(pydantic.BaseModel): + name: str + email: Union[str, None] + phone: Union[str, None] + + model_with_none = User(name="Eve", email=None, phone=None) + data = {"model": model_with_none} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Eve","email":null,"phone":null}}' + + model_with_values = User(name="Frank", email="frank@example.com", phone=None) + data = {"model": model_with_values} + json_bytes = openapi_dumps(data) + assert json_bytes == b'{"model":{"name":"Frank","email":"frank@example.com","phone":null}}' From 8d9811857f8dd173c5a9153ffbc6b34d292f9f5e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:01:11 +0000 Subject: [PATCH 09/12] codegen metadata --- .stats.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.stats.yml b/.stats.yml index b07d714..ba5ec8c 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-2452b7acf02a6afbb200a788317469e0ee1038117d6940a9813f8b72835e1ec1.yml -openapi_spec_hash: 2c61a542b0bd70f5b7c7d996c682a0c0 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-ffea0784ea7e0e97ae12efcabb2b7414d33071f821960c8f123528895ff531d0.yml +openapi_spec_hash: 185fef56dee890cc5fe995726d462caf config_hash: 71c10599b4847b7bccbc56016586d07f From 23805e34be0a2dc9897a8962a7dd33bd840ef85c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 17:48:37 +0000 Subject: [PATCH 10/12] feat(api): api update --- .stats.yml | 4 +- tests/api_resources/test_extract.py | 208 +++++++++++++++++++++++++-- tests/api_resources/test_generate.py | 208 +++++++++++++++++++++++++-- 3 files changed, 402 insertions(+), 18 deletions(-) diff --git a/.stats.yml b/.stats.yml index ba5ec8c..cd59a34 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 4 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-ffea0784ea7e0e97ae12efcabb2b7414d33071f821960c8f123528895ff531d0.yml -openapi_spec_hash: 185fef56dee890cc5fe995726d462caf +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-48ec35c63b70165a6c25cc2a32f486f4a920e40d209b0a1611b23f6f2f5a7936.yml +openapi_spec_hash: 0ec2a57c0562d4dc260dd67a141db240 config_hash: 71c10599b4847b7bccbc56016586d07f diff --git a/tests/api_resources/test_extract.py b/tests/api_resources/test_extract.py index a41439c..f5c7c99 100644 --- a/tests/api_resources/test_extract.py +++ b/tests/api_resources/test_extract.py @@ -21,7 +21,31 @@ class TestExtract: @parametrize def test_method_json(self, client: Tabstack) -> None: extract = client.extract.json( - json_schema={}, + json_schema={ + "properties": { + "stories": { + "items": { + "properties": { + "author": { + "description": "Author username", + "type": "string", + }, + "points": { + "description": "Story points", + "type": "number", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) assert_matches_type(ExtractJsonResponse, extract, path=["response"]) @@ -30,7 +54,31 @@ def test_method_json(self, client: Tabstack) -> None: @parametrize def test_method_json_with_all_params(self, client: Tabstack) -> None: extract = client.extract.json( - json_schema={}, + json_schema={ + "properties": { + "stories": { + "items": { + "properties": { + "author": { + "description": "Author username", + "type": "string", + }, + "points": { + "description": "Story points", + "type": "number", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", geo_target={"country": "US"}, nocache=False, @@ -41,7 +89,31 @@ def test_method_json_with_all_params(self, client: Tabstack) -> None: @parametrize def test_raw_response_json(self, client: Tabstack) -> None: response = client.extract.with_raw_response.json( - json_schema={}, + json_schema={ + "properties": { + "stories": { + "items": { + "properties": { + "author": { + "description": "Author username", + "type": "string", + }, + "points": { + "description": "Story points", + "type": "number", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) @@ -54,7 +126,31 @@ def test_raw_response_json(self, client: Tabstack) -> None: @parametrize def test_streaming_response_json(self, client: Tabstack) -> None: with client.extract.with_streaming_response.json( - json_schema={}, + json_schema={ + "properties": { + "stories": { + "items": { + "properties": { + "author": { + "description": "Author username", + "type": "string", + }, + "points": { + "description": "Story points", + "type": "number", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) as response: assert not response.is_closed @@ -120,7 +216,31 @@ class TestAsyncExtract: @parametrize async def test_method_json(self, async_client: AsyncTabstack) -> None: extract = await async_client.extract.json( - json_schema={}, + json_schema={ + "properties": { + "stories": { + "items": { + "properties": { + "author": { + "description": "Author username", + "type": "string", + }, + "points": { + "description": "Story points", + "type": "number", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) assert_matches_type(ExtractJsonResponse, extract, path=["response"]) @@ -129,7 +249,31 @@ async def test_method_json(self, async_client: AsyncTabstack) -> None: @parametrize async def test_method_json_with_all_params(self, async_client: AsyncTabstack) -> None: extract = await async_client.extract.json( - json_schema={}, + json_schema={ + "properties": { + "stories": { + "items": { + "properties": { + "author": { + "description": "Author username", + "type": "string", + }, + "points": { + "description": "Story points", + "type": "number", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", geo_target={"country": "US"}, nocache=False, @@ -140,7 +284,31 @@ async def test_method_json_with_all_params(self, async_client: AsyncTabstack) -> @parametrize async def test_raw_response_json(self, async_client: AsyncTabstack) -> None: response = await async_client.extract.with_raw_response.json( - json_schema={}, + json_schema={ + "properties": { + "stories": { + "items": { + "properties": { + "author": { + "description": "Author username", + "type": "string", + }, + "points": { + "description": "Story points", + "type": "number", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) @@ -153,7 +321,31 @@ async def test_raw_response_json(self, async_client: AsyncTabstack) -> None: @parametrize async def test_streaming_response_json(self, async_client: AsyncTabstack) -> None: async with async_client.extract.with_streaming_response.json( - json_schema={}, + json_schema={ + "properties": { + "stories": { + "items": { + "properties": { + "author": { + "description": "Author username", + "type": "string", + }, + "points": { + "description": "Story points", + "type": "number", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) as response: assert not response.is_closed diff --git a/tests/api_resources/test_generate.py b/tests/api_resources/test_generate.py index eb0c4c6..cc8e089 100644 --- a/tests/api_resources/test_generate.py +++ b/tests/api_resources/test_generate.py @@ -22,7 +22,31 @@ class TestGenerate: def test_method_json(self, client: Tabstack) -> None: generate = client.generate.json( instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", - json_schema={}, + json_schema={ + "properties": { + "summaries": { + "items": { + "properties": { + "category": { + "description": "Story category (tech/business/science/etc)", + "type": "string", + }, + "summary": { + "description": "One-sentence summary of the story", + "type": "string", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) assert_matches_type(GenerateJsonResponse, generate, path=["response"]) @@ -32,7 +56,31 @@ def test_method_json(self, client: Tabstack) -> None: def test_method_json_with_all_params(self, client: Tabstack) -> None: generate = client.generate.json( instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", - json_schema={}, + json_schema={ + "properties": { + "summaries": { + "items": { + "properties": { + "category": { + "description": "Story category (tech/business/science/etc)", + "type": "string", + }, + "summary": { + "description": "One-sentence summary of the story", + "type": "string", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", geo_target={"country": "US"}, nocache=False, @@ -44,7 +92,31 @@ def test_method_json_with_all_params(self, client: Tabstack) -> None: def test_raw_response_json(self, client: Tabstack) -> None: response = client.generate.with_raw_response.json( instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", - json_schema={}, + json_schema={ + "properties": { + "summaries": { + "items": { + "properties": { + "category": { + "description": "Story category (tech/business/science/etc)", + "type": "string", + }, + "summary": { + "description": "One-sentence summary of the story", + "type": "string", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) @@ -58,7 +130,31 @@ def test_raw_response_json(self, client: Tabstack) -> None: def test_streaming_response_json(self, client: Tabstack) -> None: with client.generate.with_streaming_response.json( instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", - json_schema={}, + json_schema={ + "properties": { + "summaries": { + "items": { + "properties": { + "category": { + "description": "Story category (tech/business/science/etc)", + "type": "string", + }, + "summary": { + "description": "One-sentence summary of the story", + "type": "string", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) as response: assert not response.is_closed @@ -80,7 +176,31 @@ class TestAsyncGenerate: async def test_method_json(self, async_client: AsyncTabstack) -> None: generate = await async_client.generate.json( instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", - json_schema={}, + json_schema={ + "properties": { + "summaries": { + "items": { + "properties": { + "category": { + "description": "Story category (tech/business/science/etc)", + "type": "string", + }, + "summary": { + "description": "One-sentence summary of the story", + "type": "string", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) assert_matches_type(GenerateJsonResponse, generate, path=["response"]) @@ -90,7 +210,31 @@ async def test_method_json(self, async_client: AsyncTabstack) -> None: async def test_method_json_with_all_params(self, async_client: AsyncTabstack) -> None: generate = await async_client.generate.json( instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", - json_schema={}, + json_schema={ + "properties": { + "summaries": { + "items": { + "properties": { + "category": { + "description": "Story category (tech/business/science/etc)", + "type": "string", + }, + "summary": { + "description": "One-sentence summary of the story", + "type": "string", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", geo_target={"country": "US"}, nocache=False, @@ -102,7 +246,31 @@ async def test_method_json_with_all_params(self, async_client: AsyncTabstack) -> async def test_raw_response_json(self, async_client: AsyncTabstack) -> None: response = await async_client.generate.with_raw_response.json( instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", - json_schema={}, + json_schema={ + "properties": { + "summaries": { + "items": { + "properties": { + "category": { + "description": "Story category (tech/business/science/etc)", + "type": "string", + }, + "summary": { + "description": "One-sentence summary of the story", + "type": "string", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) @@ -116,7 +284,31 @@ async def test_raw_response_json(self, async_client: AsyncTabstack) -> None: async def test_streaming_response_json(self, async_client: AsyncTabstack) -> None: async with async_client.generate.with_streaming_response.json( instructions="For each story, categorize it (tech/business/science/other) and write a one-sentence summary explaining what it's about in simple terms.", - json_schema={}, + json_schema={ + "properties": { + "summaries": { + "items": { + "properties": { + "category": { + "description": "Story category (tech/business/science/etc)", + "type": "string", + }, + "summary": { + "description": "One-sentence summary of the story", + "type": "string", + }, + "title": { + "description": "Story title", + "type": "string", + }, + }, + "type": "object", + }, + "type": "array", + } + }, + "type": "object", + }, url="https://news.ycombinator.com", ) as response: assert not response.is_closed From e139b72585ff197a48d1af52e7fd8b7ebc5f40d1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:05:40 +0000 Subject: [PATCH 11/12] feat(api): add research --- .stats.yml | 4 +- api.md | 3 +- src/tabstack/resources/agent.py | 163 +++++++++++++++++++- src/tabstack/types/__init__.py | 2 + src/tabstack/types/agent_research_params.py | 21 +++ src/tabstack/types/research_event.py | 16 ++ tests/api_resources/test_agent.py | 88 +++++++++++ 7 files changed, 293 insertions(+), 4 deletions(-) create mode 100644 src/tabstack/types/agent_research_params.py create mode 100644 src/tabstack/types/research_event.py diff --git a/.stats.yml b/.stats.yml index cd59a34..09992ec 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ -configured_endpoints: 4 +configured_endpoints: 5 openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-48ec35c63b70165a6c25cc2a32f486f4a920e40d209b0a1611b23f6f2f5a7936.yml openapi_spec_hash: 0ec2a57c0562d4dc260dd67a141db240 -config_hash: 71c10599b4847b7bccbc56016586d07f +config_hash: 73888ecc2a9b87af87e1b0d7870eab0e diff --git a/api.md b/api.md index ff046f5..48a3a76 100644 --- a/api.md +++ b/api.md @@ -3,12 +3,13 @@ Types: ```python -from tabstack.types import AutomateEvent +from tabstack.types import AutomateEvent, ResearchEvent ``` Methods: - client.agent.automate(\*\*params) -> AutomateEvent +- client.agent.research(\*\*params) -> ResearchEvent # Extract diff --git a/src/tabstack/resources/agent.py b/src/tabstack/resources/agent.py index e303de4..0cf85d7 100644 --- a/src/tabstack/resources/agent.py +++ b/src/tabstack/resources/agent.py @@ -2,9 +2,11 @@ from __future__ import annotations +from typing_extensions import Literal + import httpx -from ..types import agent_automate_params +from ..types import agent_automate_params, agent_research_params from .._types import Body, Omit, Query, Headers, NotGiven, omit, not_given from .._utils import maybe_transform, async_maybe_transform from .._compat import cached_property @@ -18,6 +20,7 @@ from .._streaming import Stream, AsyncStream from .._base_client import make_request_options from ..types.automate_event import AutomateEvent +from ..types.research_event import ResearchEvent __all__ = ["AgentResource", "AsyncAgentResource"] @@ -127,6 +130,79 @@ def automate( stream_cls=Stream[AutomateEvent], ) + def research( + self, + *, + query: str, + fetch_timeout: int | Omit = omit, + mode: Literal["fast", "balanced"] | 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> Stream[ResearchEvent]: + """ + Execute AI-powered research queries that search the web, analyze sources, and + synthesize comprehensive answers. This endpoint **always streams** responses + using Server-Sent Events (SSE). + + **Streaming Response:** + + - All responses are streamed using Server-Sent Events (`text/event-stream`) + - Real-time progress updates as research progresses through phases + + **Research Modes:** + + - `fast` - Quick answers with minimal web searches + - `balanced` - Standard research with multiple iterations (default) + + **Use Cases:** + + - Answering complex questions with cited sources + - Synthesizing information from multiple web sources + - Research reports on specific topics + - Fact-checking and verification tasks + + Args: + query: The research query or question to answer + + fetch_timeout: Timeout in seconds for fetching web pages + + mode: Research mode: fast (quick answers), balanced (standard research, default) + + nocache: Skip cache and force fresh research + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return self._post( + "/research", + body=maybe_transform( + { + "query": query, + "fetch_timeout": fetch_timeout, + "mode": mode, + "nocache": nocache, + }, + agent_research_params.AgentResearchParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ResearchEvent, + stream=True, + stream_cls=Stream[ResearchEvent], + ) + class AsyncAgentResource(AsyncAPIResource): @cached_property @@ -233,6 +309,79 @@ async def automate( stream_cls=AsyncStream[AutomateEvent], ) + async def research( + self, + *, + query: str, + fetch_timeout: int | Omit = omit, + mode: Literal["fast", "balanced"] | 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. + extra_headers: Headers | None = None, + extra_query: Query | None = None, + extra_body: Body | None = None, + timeout: float | httpx.Timeout | None | NotGiven = not_given, + ) -> AsyncStream[ResearchEvent]: + """ + Execute AI-powered research queries that search the web, analyze sources, and + synthesize comprehensive answers. This endpoint **always streams** responses + using Server-Sent Events (SSE). + + **Streaming Response:** + + - All responses are streamed using Server-Sent Events (`text/event-stream`) + - Real-time progress updates as research progresses through phases + + **Research Modes:** + + - `fast` - Quick answers with minimal web searches + - `balanced` - Standard research with multiple iterations (default) + + **Use Cases:** + + - Answering complex questions with cited sources + - Synthesizing information from multiple web sources + - Research reports on specific topics + - Fact-checking and verification tasks + + Args: + query: The research query or question to answer + + fetch_timeout: Timeout in seconds for fetching web pages + + mode: Research mode: fast (quick answers), balanced (standard research, default) + + nocache: Skip cache and force fresh research + + extra_headers: Send extra headers + + extra_query: Add additional query parameters to the request + + extra_body: Add additional JSON properties to the request + + timeout: Override the client-level default timeout for this request, in seconds + """ + extra_headers = {"Accept": "text/event-stream", **(extra_headers or {})} + return await self._post( + "/research", + body=await async_maybe_transform( + { + "query": query, + "fetch_timeout": fetch_timeout, + "mode": mode, + "nocache": nocache, + }, + agent_research_params.AgentResearchParams, + ), + options=make_request_options( + extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout + ), + cast_to=ResearchEvent, + stream=True, + stream_cls=AsyncStream[ResearchEvent], + ) + class AgentResourceWithRawResponse: def __init__(self, agent: AgentResource) -> None: @@ -241,6 +390,9 @@ def __init__(self, agent: AgentResource) -> None: self.automate = to_raw_response_wrapper( agent.automate, ) + self.research = to_raw_response_wrapper( + agent.research, + ) class AsyncAgentResourceWithRawResponse: @@ -250,6 +402,9 @@ def __init__(self, agent: AsyncAgentResource) -> None: self.automate = async_to_raw_response_wrapper( agent.automate, ) + self.research = async_to_raw_response_wrapper( + agent.research, + ) class AgentResourceWithStreamingResponse: @@ -259,6 +414,9 @@ def __init__(self, agent: AgentResource) -> None: self.automate = to_streamed_response_wrapper( agent.automate, ) + self.research = to_streamed_response_wrapper( + agent.research, + ) class AsyncAgentResourceWithStreamingResponse: @@ -268,3 +426,6 @@ def __init__(self, agent: AsyncAgentResource) -> None: self.automate = async_to_streamed_response_wrapper( agent.automate, ) + self.research = async_to_streamed_response_wrapper( + agent.research, + ) diff --git a/src/tabstack/types/__init__.py b/src/tabstack/types/__init__.py index 1f84a44..a0f24b4 100644 --- a/src/tabstack/types/__init__.py +++ b/src/tabstack/types/__init__.py @@ -3,9 +3,11 @@ from __future__ import annotations from .automate_event import AutomateEvent as AutomateEvent +from .research_event import ResearchEvent as ResearchEvent from .extract_json_params import ExtractJsonParams as ExtractJsonParams from .generate_json_params import GenerateJsonParams as GenerateJsonParams from .agent_automate_params import AgentAutomateParams as AgentAutomateParams +from .agent_research_params import AgentResearchParams as AgentResearchParams from .extract_json_response import ExtractJsonResponse as ExtractJsonResponse from .generate_json_response import GenerateJsonResponse as GenerateJsonResponse from .extract_markdown_params import ExtractMarkdownParams as ExtractMarkdownParams diff --git a/src/tabstack/types/agent_research_params.py b/src/tabstack/types/agent_research_params.py new file mode 100644 index 0000000..10741a9 --- /dev/null +++ b/src/tabstack/types/agent_research_params.py @@ -0,0 +1,21 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from __future__ import annotations + +from typing_extensions import Literal, Required, TypedDict + +__all__ = ["AgentResearchParams"] + + +class AgentResearchParams(TypedDict, total=False): + query: Required[str] + """The research query or question to answer""" + + fetch_timeout: int + """Timeout in seconds for fetching web pages""" + + mode: Literal["fast", "balanced"] + """Research mode: fast (quick answers), balanced (standard research, default)""" + + nocache: bool + """Skip cache and force fresh research""" diff --git a/src/tabstack/types/research_event.py b/src/tabstack/types/research_event.py new file mode 100644 index 0000000..04ee453 --- /dev/null +++ b/src/tabstack/types/research_event.py @@ -0,0 +1,16 @@ +# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details. + +from typing import Optional +from typing_extensions import Literal + +from .._models import BaseModel + +__all__ = ["ResearchEvent"] + + +class ResearchEvent(BaseModel): + data: Optional[object] = None + """Event payload data""" + + event: Optional[Literal["phase", "progress", "complete", "error"]] = None + """The event type: phase, progress, complete, or error""" diff --git a/tests/api_resources/test_agent.py b/tests/api_resources/test_agent.py index 62cb8ee..2174d9d 100644 --- a/tests/api_resources/test_agent.py +++ b/tests/api_resources/test_agent.py @@ -62,6 +62,50 @@ def test_streaming_response_automate(self, client: Tabstack) -> None: assert cast(Any, response.is_closed) is True + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_method_research(self, client: Tabstack) -> None: + agent_stream = client.agent.research( + query="What are the latest developments in quantum computing?", + ) + agent_stream.response.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_method_research_with_all_params(self, client: Tabstack) -> None: + agent_stream = client.agent.research( + query="What are the latest developments in quantum computing?", + fetch_timeout=30, + mode="balanced", + nocache=False, + ) + agent_stream.response.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_raw_response_research(self, client: Tabstack) -> None: + response = client.agent.with_raw_response.research( + query="What are the latest developments in quantum computing?", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = response.parse() + stream.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + def test_streaming_response_research(self, client: Tabstack) -> None: + with client.agent.with_streaming_response.research( + query="What are the latest developments in quantum computing?", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = response.parse() + stream.close() + + assert cast(Any, response.is_closed) is True + class TestAsyncAgent: parametrize = pytest.mark.parametrize( @@ -114,3 +158,47 @@ async def test_streaming_response_automate(self, async_client: AsyncTabstack) -> await stream.close() assert cast(Any, response.is_closed) is True + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_method_research(self, async_client: AsyncTabstack) -> None: + agent_stream = await async_client.agent.research( + query="What are the latest developments in quantum computing?", + ) + await agent_stream.response.aclose() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_method_research_with_all_params(self, async_client: AsyncTabstack) -> None: + agent_stream = await async_client.agent.research( + query="What are the latest developments in quantum computing?", + fetch_timeout=30, + mode="balanced", + nocache=False, + ) + await agent_stream.response.aclose() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_raw_response_research(self, async_client: AsyncTabstack) -> None: + response = await async_client.agent.with_raw_response.research( + query="What are the latest developments in quantum computing?", + ) + + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + stream = await response.parse() + await stream.close() + + @pytest.mark.skip(reason="Prism doesn't support text/event-stream responses") + @parametrize + async def test_streaming_response_research(self, async_client: AsyncTabstack) -> None: + async with async_client.agent.with_streaming_response.research( + query="What are the latest developments in quantum computing?", + ) as response: + assert not response.is_closed + assert response.http_request.headers.get("X-Stainless-Lang") == "python" + + stream = await response.parse() + await stream.close() + + assert cast(Any, response.is_closed) is True From f9389c0fb7188ba4a7a32c9853d34940b75e0a06 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 30 Jan 2026 17:06:24 +0000 Subject: [PATCH 12/12] release: 2.1.0 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ pyproject.toml | 2 +- src/tabstack/_version.py | 2 +- 4 files changed, 27 insertions(+), 3 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 65f558e..656a2ef 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "2.0.0" + ".": "2.1.0" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index d3884af..93644d7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## 2.1.0 (2026-01-30) + +Full Changelog: [v2.0.0...v2.1.0](https://github.com/Mozilla-Ocho/tabstack-python/compare/v2.0.0...v2.1.0) + +### Features + +* **api:** add research ([e139b72](https://github.com/Mozilla-Ocho/tabstack-python/commit/e139b72585ff197a48d1af52e7fd8b7ebc5f40d1)) +* **api:** api update ([23805e3](https://github.com/Mozilla-Ocho/tabstack-python/commit/23805e34be0a2dc9897a8962a7dd33bd840ef85c)) +* **api:** api update ([ac0c746](https://github.com/Mozilla-Ocho/tabstack-python/commit/ac0c74625af51293f79dbca80540781a4589adab)) +* **api:** api update ([b8a1e09](https://github.com/Mozilla-Ocho/tabstack-python/commit/b8a1e095bd6153948c18a3b15324fd0166cf136c)) +* **api:** api update ([6eb4a9f](https://github.com/Mozilla-Ocho/tabstack-python/commit/6eb4a9feb9f79e9580d5a521de46bb07f23db928)) +* **client:** add custom JSON encoder for extended type support ([4b5ce35](https://github.com/Mozilla-Ocho/tabstack-python/commit/4b5ce35a61dcc1e1c60d5d6dbfea22311c663cdb)) + + +### Bug Fixes + +* **docs:** fix mcp installation instructions for remote servers ([c8fba67](https://github.com/Mozilla-Ocho/tabstack-python/commit/c8fba6725a642e10179f6a0006642f5dfe281c3c)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([b6d62f6](https://github.com/Mozilla-Ocho/tabstack-python/commit/b6d62f664f7cabaa8201fcb02fe50d307e82553c)) +* **internal:** update `actions/checkout` version ([9197068](https://github.com/Mozilla-Ocho/tabstack-python/commit/9197068bb9bd06db9e54a49690e25e9c0f93b230)) + ## 2.0.0 (2026-01-16) Full Changelog: [v0.0.1...v2.0.0](https://github.com/Mozilla-Ocho/tabstack-python/compare/v0.0.1...v2.0.0) diff --git a/pyproject.toml b/pyproject.toml index 633b9ac..1b330a6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "tabstack" -version = "2.0.0" +version = "2.1.0" description = "The official Python library for the tabstack API" dynamic = ["readme"] license = "Apache-2.0" diff --git a/src/tabstack/_version.py b/src/tabstack/_version.py index 067869d..f351b16 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.0.0" # x-release-please-version +__version__ = "2.1.0" # x-release-please-version