Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 10 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
name: CI
on:
push:
branches-ignore:
- 'generated'
- 'codegen/**'
- 'integrated/**'
- 'stl-preview-head/**'
- 'stl-preview-base/**'
branches:
- '**'
- '!integrated/**'
- '!stl-preview-head/**'
- '!stl-preview-base/**'
- '!generated'
- '!codegen/**'
- 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
Expand All @@ -17,7 +19,7 @@ jobs:
timeout-minutes: 10
name: lint
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
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
steps:
- uses: actions/checkout@v6

Expand All @@ -33,7 +35,7 @@ jobs:
run: ./scripts/lint

build:
if: github.event_name == 'push' || github.event.pull_request.head.repo.fork
if: (github.event_name == 'push' || github.event.pull_request.head.repo.fork) && (github.event_name != 'push' || github.event.head_commit.message != 'codegen metadata')
timeout-minutes: 10
name: build
permissions:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
.prism.log
.stdy.log
_dev

__pycache__
Expand Down
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "2.3.0"
".": "2.4.0"
}
8 changes: 4 additions & 4 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 5
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-8f80078ef30bc395d44843f9ce076188ba43a297e158d5f3672c45dc814ffcf0.yml
openapi_spec_hash: c615a817dcb27c55bd15b9b9346669c2
config_hash: 6b388b673b2a48895ab10da245fb6771
configured_endpoints: 6
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/mozilla%2Ftabstack-ab135cad4c10edc98bbe45d18a1ca23d2d4aa4b2ba2e4554e6991e322ce46231.yml
openapi_spec_hash: 3e8185dfbbd4e83d5f05ded9ce9c5b06
config_hash: 57c64e5e8fe99c1bd7af536d82af4ad9
29 changes: 29 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,34 @@
# Changelog

## 2.4.0 (2026-04-10)

Full Changelog: [v2.3.0...v2.4.0](https://github.com/Mozilla-Ocho/tabstack-python/compare/v2.3.0...v2.4.0)

### Features

* **api:** add input endpoint ([f3b1f16](https://github.com/Mozilla-Ocho/tabstack-python/commit/f3b1f16204c0d724193815c5e34100831f0a2653))
* **api:** api update ([cda5c19](https://github.com/Mozilla-Ocho/tabstack-python/commit/cda5c19baedee455072a8071cb2741714a9ded19))
* **api:** api update ([ed80ce0](https://github.com/Mozilla-Ocho/tabstack-python/commit/ed80ce06975480e8466b18b75397ee3c761f6398))
* **api:** better handling of SSE events ([6b6ba7b](https://github.com/Mozilla-Ocho/tabstack-python/commit/6b6ba7b5b6a6aba119a6caf6aec3af5ff5c6350b))
* **internal:** implement indices array format for query and form serialization ([97a9bb7](https://github.com/Mozilla-Ocho/tabstack-python/commit/97a9bb7241ad0c7a98483d81f74bbb5ee40e1b93))


### Bug Fixes

* **client:** preserve hardcoded query params when merging with user params ([2cda6ae](https://github.com/Mozilla-Ocho/tabstack-python/commit/2cda6aeb86a50115ba347549452c57700e810679))
* **deps:** bump minimum typing-extensions version ([1946504](https://github.com/Mozilla-Ocho/tabstack-python/commit/194650472a951f76bd180e1dd08507fcf2670577))
* ensure file data are only sent as 1 parameter ([10f77bc](https://github.com/Mozilla-Ocho/tabstack-python/commit/10f77bcad1e0ea05151f291bf55b17a04512465a))
* **pydantic:** do not pass `by_alias` unless set ([a32962e](https://github.com/Mozilla-Ocho/tabstack-python/commit/a32962e54563d7073726b4463d70017fc2a40331))
* sanitize endpoint path params ([f4632c7](https://github.com/Mozilla-Ocho/tabstack-python/commit/f4632c74a42a3ea56998e6f9d9afacace4bd4a5c))


### Chores

* **ci:** skip lint on metadata-only changes ([9710840](https://github.com/Mozilla-Ocho/tabstack-python/commit/97108400820fbe715dcb922c997b87b612bf588e))
* configure new SDK language ([f5f587a](https://github.com/Mozilla-Ocho/tabstack-python/commit/f5f587ab07a757e4ada42e6bc3c802f29591c536))
* **internal:** tweak CI branches ([8458777](https://github.com/Mozilla-Ocho/tabstack-python/commit/8458777f42fdbdc611d213513b1e7f026886ba42))
* **internal:** update gitignore ([e39102c](https://github.com/Mozilla-Ocho/tabstack-python/commit/e39102cd8c44fbbaec52a5bed74a11deab25b97f))

## 2.3.0 (2026-03-12)

Full Changelog: [v2.2.0...v2.3.0](https://github.com/Mozilla-Ocho/tabstack-python/compare/v2.2.0...v2.3.0)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=%40tabstack%2Fmcp&config=eyJuYW1lIjoiQHRhYnN0YWNrL21jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL3RhYnN0YWNrLnN0bG1jcC5jb20iLCJoZWFkZXJzIjp7IngtdGFic3RhY2stYXBpLWtleSI6Ik15IEFQSSBLZXkifX0)
[![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%22%40tabstack%2Fmcp%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)
[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=%40tabstack%2Fsdk-mcp&config=eyJuYW1lIjoiQHRhYnN0YWNrL3Nkay1tY3AiLCJ0cmFuc3BvcnQiOiJodHRwIiwidXJsIjoiaHR0cHM6Ly90YWJzdGFjay5zdGxtY3AuY29tIiwiaGVhZGVycyI6eyJ4LXRhYnN0YWNrLWFwaS1rZXkiOiJNeSBBUEkgS2V5In19)
[![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%22%40tabstack%2Fsdk-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.

Expand Down
3 changes: 2 additions & 1 deletion api.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@
Types:

```python
from tabstack.types import AutomateEvent, ResearchEvent
from tabstack.types import AutomateEvent, ResearchEvent, AgentAutomateInputResponse
```

Methods:

- <code title="post /automate">client.agent.<a href="./src/tabstack/resources/agent.py">automate</a>(\*\*<a href="src/tabstack/types/agent_automate_params.py">params</a>) -> <a href="./src/tabstack/types/automate_event.py">AutomateEvent</a></code>
- <code title="post /automate/{requestID}/input">client.agent.<a href="./src/tabstack/resources/agent.py">automate_input</a>(request_id, \*\*<a href="src/tabstack/types/agent_automate_input_params.py">params</a>) -> <a href="./src/tabstack/types/agent_automate_input_response.py">AgentAutomateInputResponse</a></code>
- <code title="post /research">client.agent.<a href="./src/tabstack/resources/agent.py">research</a>(\*\*<a href="src/tabstack/types/agent_research_params.py">params</a>) -> <a href="./src/tabstack/types/research_event.py">ResearchEvent</a></code>

# Extract
Expand Down
4 changes: 2 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "tabstack"
version = "2.3.0"
version = "2.4.0"
description = "The official Python library for the tabstack API"
dynamic = ["readme"]
license = "Apache-2.0"
Expand All @@ -11,7 +11,7 @@ authors = [
dependencies = [
"httpx>=0.23.0, <1",
"pydantic>=1.9.0, <3",
"typing-extensions>=4.10, <5",
"typing-extensions>=4.14, <5",
"anyio>=3.5.0, <5",
"distro>=1.7.0, <2",
"sniffio",
Expand Down
8 changes: 8 additions & 0 deletions src/tabstack/_base_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -540,6 +540,10 @@ def _build_request(
files = cast(HttpxRequestFiles, ForceMultipartDict())

prepared_url = self._prepare_url(options.url)
# preserve hard-coded query params from the url
if params and prepared_url.query:
params = {**dict(prepared_url.params.items()), **params}
prepared_url = prepared_url.copy_with(raw_path=prepared_url.raw_path.split(b"?", 1)[0])
if "_" in prepared_url.host:
# work around https://github.com/encode/httpx/discussions/2880
kwargs["extensions"] = {"sni_hostname": prepared_url.host.replace("_", "-")}
Expand Down Expand Up @@ -1952,6 +1956,7 @@ def make_request_options(
idempotency_key: str | None = None,
timeout: float | httpx.Timeout | None | NotGiven = not_given,
post_parser: PostParser | NotGiven = not_given,
synthesize_event_and_data: bool | None = None,
) -> RequestOptions:
"""Create a dict of type RequestOptions without keys of NotGiven values."""
options: RequestOptions = {}
Expand All @@ -1977,6 +1982,9 @@ def make_request_options(
# internal
options["post_parser"] = post_parser # type: ignore

if synthesize_event_and_data is not None:
options["synthesize_event_and_data"] = synthesize_event_and_data

return options


Expand Down
11 changes: 9 additions & 2 deletions src/tabstack/_compat.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
from datetime import date, datetime
from typing_extensions import Self, Literal
from typing_extensions import Self, Literal, TypedDict

import pydantic
from pydantic.fields import FieldInfo
Expand Down Expand Up @@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
return model.model_dump_json(indent=indent)


class _ModelDumpKwargs(TypedDict, total=False):
by_alias: bool


def model_dump(
model: pydantic.BaseModel,
*,
Expand All @@ -142,14 +146,17 @@ def model_dump(
by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
kwargs: _ModelDumpKwargs = {}
if by_alias is not None:
kwargs["by_alias"] = by_alias
return model.model_dump(
mode=mode,
exclude=exclude,
exclude_unset=exclude_unset,
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
by_alias=by_alias,
**kwargs,
)
return cast(
"dict[str, Any]",
Expand Down
2 changes: 2 additions & 0 deletions src/tabstack/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,7 @@ class FinalRequestOptionsInput(TypedDict, total=False):
json_data: Body
extra_json: AnyMapping
follow_redirects: bool
synthesize_event_and_data: bool


@final
Expand All @@ -818,6 +819,7 @@ class FinalRequestOptions(pydantic.BaseModel):
idempotency_key: Union[str, None] = None
post_parser: Union[Callable[[Any], Any], NotGiven] = NotGiven()
follow_redirects: Union[bool, None] = None
synthesize_event_and_data: Optional[bool] = None

content: Union[bytes, bytearray, IO[bytes], Iterable[bytes], AsyncIterable[bytes], None] = None
# It should be noted that we cannot use `json` here as that would override
Expand Down
5 changes: 4 additions & 1 deletion src/tabstack/_qs.py
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,10 @@ def _stringify_item(
items.extend(self._stringify_item(key, item, opts))
return items
elif array_format == "indices":
raise NotImplementedError("The array indices format is not supported yet")
items = []
for i, item in enumerate(value):
items.extend(self._stringify_item(f"{key}[{i}]", item, opts))
return items
elif array_format == "brackets":
items = []
key = key + "[]"
Expand Down
16 changes: 14 additions & 2 deletions src/tabstack/_streaming.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,13 @@ def __stream__(self) -> Iterator[_T]:

try:
for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
yield process_data(
data={"data": sse.json(), "event": sse.event}
if self._options is not None and self._options.synthesize_event_and_data
else sse.json(),
cast_to=cast_to,
response=response,
)
finally:
# Ensure the response is closed even if the consumer doesn't read all data
response.close()
Expand Down Expand Up @@ -125,7 +131,13 @@ async def __stream__(self) -> AsyncIterator[_T]:

try:
async for sse in iterator:
yield process_data(data=sse.json(), cast_to=cast_to, response=response)
yield process_data(
data={"data": sse.json(), "event": sse.event}
if self._options is not None and self._options.synthesize_event_and_data
else sse.json(),
cast_to=cast_to,
response=response,
)
finally:
# Ensure the response is closed even if the consumer doesn't read all data
await response.aclose()
Expand Down
1 change: 1 addition & 0 deletions src/tabstack/_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ class RequestOptions(TypedDict, total=False):
extra_json: AnyMapping
idempotency_key: str
follow_redirects: bool
synthesize_event_and_data: bool


# Sentinel class used until PEP 0661 is accepted
Expand Down
1 change: 1 addition & 0 deletions src/tabstack/_utils/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from ._path import path_template as path_template
from ._sync import asyncify as asyncify
from ._proxy import LazyProxy as LazyProxy
from ._utils import (
Expand Down
Loading
Loading