diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index edf03df..051a17f 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
@@ -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());
@@ -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: |
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/.stats.yml b/.stats.yml
index 026a0e1..09992ec 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
+configured_endpoints: 5
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-48ec35c63b70165a6c25cc2a32f486f4a920e40d209b0a1611b23f6f2f5a7936.yml
+openapi_spec_hash: 0ec2a57c0562d4dc260dd67a141db240
+config_hash: 73888ecc2a9b87af87e1b0d7870eab0e
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/README.md b/README.md
index 5d13168..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.
-[](https://cursor.com/en-US/install-mcp?name=tabstack-mcp&config=eyJuYW1lIjoidGFic3RhY2stbWNwIiwidHJhbnNwb3J0Ijoic3NlIiwidXJsIjoiaHR0cHM6Ly90YWJzdGFjay5zdGxtY3AuY29tL3NzZSJ9)
-[](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)
+[](https://cursor.com/en-US/install-mcp?name=tabstack-mcp&config=eyJuYW1lIjoidGFic3RhY2stbWNwIiwidHJhbnNwb3J0IjoiaHR0cCIsInVybCI6Imh0dHBzOi8vdGFic3RhY2suc3RsbWNwLmNvbSIsImhlYWRlcnMiOnsieC10YWJzdGFjay1hcGkta2V5IjoiTXkgQVBJIEtleSJ9fQ)
+[](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.
@@ -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",
+ geo_target={},
+)
+print(automate_event.geo_target)
+```
+
## 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/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/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/_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/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
diff --git a/src/tabstack/resources/agent.py b/src/tabstack/resources/agent.py
index 72856e1..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"]
@@ -47,6 +50,7 @@ def automate(
*,
task: str,
data: object | 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,
@@ -58,16 +62,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 +89,8 @@ def automate(
data: JSON data to provide context for form filling or complex tasks
+ geo_target: Optional geotargeting parameters for proxy requests
+
guardrails: Safety constraints for execution
max_iterations: Maximum task iterations
@@ -104,6 +114,7 @@ def automate(
{
"task": task,
"data": data,
+ "geo_target": geo_target,
"guardrails": guardrails,
"max_iterations": max_iterations,
"max_validation_attempts": max_validation_attempts,
@@ -119,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
@@ -145,6 +229,7 @@ async def automate(
*,
task: str,
data: object | 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,
@@ -156,16 +241,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 +268,8 @@ async def automate(
data: JSON data to provide context for form filling or complex tasks
+ geo_target: Optional geotargeting parameters for proxy requests
+
guardrails: Safety constraints for execution
max_iterations: Maximum task iterations
@@ -202,6 +293,7 @@ async def automate(
{
"task": task,
"data": data,
+ "geo_target": geo_target,
"guardrails": guardrails,
"max_iterations": max_iterations,
"max_validation_attempts": max_validation_attempts,
@@ -217,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:
@@ -225,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:
@@ -234,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:
@@ -243,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:
@@ -252,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/resources/extract.py b/src/tabstack/resources/extract.py
index 2805fd6..20984a8 100644
--- a/src/tabstack/resources/extract.py
+++ b/src/tabstack/resources/extract.py
@@ -47,6 +47,7 @@ def json(
*,
json_schema: object,
url: str,
+ 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.
@@ -63,6 +64,8 @@ def json(
url: URL to fetch and extract data from
+ geo_target: 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,
+ "geo_target": geo_target,
"nocache": nocache,
},
extract_json_params.ExtractJsonParams,
@@ -93,6 +97,7 @@ def markdown(
self,
*,
url: str,
+ 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.
@@ -109,6 +114,8 @@ def markdown(
Args:
url: URL to fetch and convert to markdown
+ geo_target: 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,
+ "geo_target": geo_target,
"metadata": metadata,
"nocache": nocache,
},
@@ -164,6 +172,7 @@ async def json(
*,
json_schema: object,
url: str,
+ 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.
@@ -180,6 +189,8 @@ async def json(
url: URL to fetch and extract data from
+ geo_target: 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,
+ "geo_target": geo_target,
"nocache": nocache,
},
extract_json_params.ExtractJsonParams,
@@ -210,6 +222,7 @@ async def markdown(
self,
*,
url: str,
+ 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.
@@ -226,6 +239,8 @@ async def markdown(
Args:
url: URL to fetch and convert to markdown
+ geo_target: 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,
+ "geo_target": geo_target,
"metadata": metadata,
"nocache": nocache,
},
diff --git a/src/tabstack/resources/generate.py b/src/tabstack/resources/generate.py
index 36d0c4d..49aabd2 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,
+ 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.
@@ -66,6 +67,8 @@ def json(
url: URL to fetch content from
+ geo_target: 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,
+ "geo_target": geo_target,
"nocache": nocache,
},
generate_json_params.GenerateJsonParams,
@@ -120,6 +124,7 @@ async def json(
instructions: str,
json_schema: object,
url: str,
+ 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.
@@ -139,6 +144,8 @@ async def json(
url: URL to fetch content from
+ geo_target: 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,
+ "geo_target": geo_target,
"nocache": nocache,
},
generate_json_params.GenerateJsonParams,
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_automate_params.py b/src/tabstack/types/agent_automate_params.py
index e36d15b..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"]
+__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"""
+ geo_target: 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/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/extract_json_params.py b/src/tabstack/types/extract_json_params.py
index 58aa6b0..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"]
+__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"""
+ geo_target: 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..a13dc4d 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"""
+ geo_target: 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/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"""
diff --git a/src/tabstack/types/generate_json_params.py b/src/tabstack/types/generate_json_params.py
index 3158b97..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"]
+__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"""
+ geo_target: 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/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 9cc9f8e..2174d9d 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={},
+ geo_target={"country": "US"},
guardrails="browse and extract only, don't interact with repositories",
max_iterations=50,
max_validation_attempts=3,
@@ -61,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(
@@ -81,6 +126,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={},
+ geo_target={"country": "US"},
guardrails="browse and extract only, don't interact with repositories",
max_iterations=50,
max_validation_attempts=3,
@@ -112,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
diff --git a/tests/api_resources/test_extract.py b/tests/api_resources/test_extract.py
index 4590a4a..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,8 +54,33 @@ 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,
)
assert_matches_type(ExtractJsonResponse, extract, path=["response"])
@@ -40,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",
)
@@ -53,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
@@ -77,6 +174,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",
+ geo_target={"country": "US"},
metadata=True,
nocache=False,
)
@@ -118,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"])
@@ -127,8 +249,33 @@ 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,
)
assert_matches_type(ExtractJsonResponse, extract, path=["response"])
@@ -137,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",
)
@@ -150,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
@@ -174,6 +369,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",
+ 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 7e1e167..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,8 +56,33 @@ 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,
)
assert_matches_type(GenerateJsonResponse, generate, path=["response"])
@@ -43,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",
)
@@ -57,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
@@ -79,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"])
@@ -89,8 +210,33 @@ 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,
)
assert_matches_type(GenerateJsonResponse, generate, path=["response"])
@@ -100,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",
)
@@ -114,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
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}}'