Skip to content
This repository was archived by the owner on Apr 7, 2026. It is now read-only.

Commit bab6809

Browse files
committed
wip
1 parent 2bef575 commit bab6809

41 files changed

Lines changed: 4170 additions & 4114 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

pyproject.toml

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,28 @@
11
[project]
2-
name = "streamstore"
3-
version = "5.0.0"
2+
name = "s2-sdk"
3+
version = "0.1.0"
44
description = "Python SDK for s2.dev"
55
readme = "README.md"
66
license = "MIT"
77
license-files = ["LICENSE"]
88
requires-python = ">=3.11"
99
dependencies = [
10-
"grpcio-tools>=1.69.0",
11-
"grpcio>=1.69.0",
12-
"types-protobuf>=5.29.1.20241207",
13-
"grpc-stubs>=1.53.0.5",
10+
"httpx[http2]>=0.28.0",
11+
"protobuf>=5.29.0",
12+
"pydantic>=2.0",
1413
"anyio>=4.8.0",
14+
"zstandard>=0.23.0",
1515
]
1616

1717
[dependency-groups]
18-
dev = ["mypy>=1.14.1", "poethepoet>=0.36.0", "ruff>=0.9.1"]
18+
dev = [
19+
"datamodel-code-generator>=0.28.0",
20+
"grpcio-tools>=1.69.0",
21+
"mypy>=1.14.1",
22+
"poethepoet>=0.36.0",
23+
"ruff>=0.9.1",
24+
"types-protobuf>=5.29.1.20241207",
25+
]
1926
test = [
2027
"pytest>=8.0.0",
2128
"pytest-asyncio>=0.23.0",
@@ -34,8 +41,12 @@ docs = [
3441
requires = ["hatchling"]
3542
build-backend = "hatchling.build"
3643

44+
[tool.hatch.build.targets.wheel]
45+
packages = ["src/s2_sdk"]
46+
3747
[tool.mypy]
38-
files = ["src/", "tests/", "examples/"]
48+
files = ["src/s2_sdk/", "tests/"]
49+
exclude = ["src/s2_sdk/_generated/"]
3950

4051
[tool.ruff]
4152
exclude = [
@@ -59,7 +70,7 @@ ci_linter = "uv run ruff check"
5970
ci_formatter = "uv run ruff format --check"
6071
checker = ["linter", "formatter", "type_checker"]
6172
ci_checker = ["ci_linter", "ci_formatter", "type_checker"]
62-
e2e_tests = "uv run pytest tests/ -v -s"
73+
e2e_tests = "uv run pytest tests/ -v -s -m 'account or basin or stream'"
6374
e2e_account_tests = "uv run pytest tests/ -v -s -m account"
6475
e2e_basin_tests = "uv run pytest tests/ -v -s -m basin"
6576
e2e_stream_tests = "uv run pytest tests/ -v -s -m stream"

src/s2_sdk/__init__.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
__all__ = [
2+
"S2",
3+
"S2Basin",
4+
"S2Stream",
5+
"S2Error",
6+
"S2ApiError",
7+
"AppendConditionFailed",
8+
"S2SessionError",
9+
"Endpoints",
10+
"s2_sdk.types",
11+
"s2_sdk.utils",
12+
]
13+
14+
from s2_sdk._endpoints import Endpoints
15+
from s2_sdk._exceptions import (
16+
AppendConditionFailed,
17+
S2ApiError,
18+
S2Error,
19+
S2SessionError,
20+
)
21+
from s2_sdk._ops import S2, S2Basin, S2Stream

src/s2_sdk/_client.py

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
from importlib.metadata import version
2+
from typing import Any
3+
4+
import httpx
5+
6+
from s2_sdk._exceptions import AppendConditionFailed, S2ApiError
7+
8+
_VERSION = version("s2-sdk")
9+
_USER_AGENT = f"s2-sdk-python/{_VERSION}"
10+
11+
12+
class HttpClient:
13+
__slots__ = ("_client",)
14+
15+
def __init__(
16+
self,
17+
base_url: str,
18+
access_token: str,
19+
timeout: float,
20+
http2: bool = True,
21+
) -> None:
22+
self._client = httpx.AsyncClient(
23+
base_url=base_url,
24+
headers={
25+
"authorization": f"Bearer {access_token}",
26+
"user-agent": _USER_AGENT,
27+
},
28+
timeout=timeout,
29+
http2=http2,
30+
)
31+
32+
async def request(
33+
self,
34+
method: str,
35+
path: str,
36+
*,
37+
json: Any = None,
38+
params: dict[str, Any] | None = None,
39+
headers: dict[str, str] | None = None,
40+
content: bytes | None = None,
41+
) -> httpx.Response:
42+
response = await self._client.request(
43+
method,
44+
path,
45+
json=json,
46+
params=params,
47+
headers=headers,
48+
content=content,
49+
)
50+
_raise_for_status(response)
51+
return response
52+
53+
def stream(
54+
self,
55+
method: str,
56+
path: str,
57+
*,
58+
params: dict[str, Any] | None = None,
59+
headers: dict[str, str] | None = None,
60+
):
61+
return self._client.stream(
62+
method,
63+
path,
64+
params=params,
65+
headers=headers,
66+
)
67+
68+
async def close(self) -> None:
69+
await self._client.aclose()
70+
71+
72+
def _raise_for_status(response: httpx.Response) -> None:
73+
status = response.status_code
74+
if 200 <= status < 300:
75+
return
76+
77+
if status == 412:
78+
body = response.json()
79+
if "fencing_token_mismatch" in body:
80+
raise AppendConditionFailed(
81+
f"Fencing token mismatch: {body['fencing_token_mismatch']}",
82+
status_code=status,
83+
)
84+
elif "seq_num_mismatch" in body:
85+
raise AppendConditionFailed(
86+
f"Sequence number mismatch: {body['seq_num_mismatch']}",
87+
status_code=status,
88+
)
89+
raise AppendConditionFailed(str(body), status_code=status)
90+
91+
if status == 416:
92+
# Tail response — not an error, handled by callers
93+
return
94+
95+
try:
96+
body = response.json()
97+
message = body.get("message", response.text)
98+
code = body.get("code")
99+
except Exception:
100+
message = response.text
101+
code = None
102+
103+
raise S2ApiError(message, status_code=status, code=code)

src/s2_sdk/_endpoints.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import os
2+
3+
from s2_sdk._exceptions import fallible
4+
5+
6+
class Endpoints:
7+
"""S2 endpoints."""
8+
9+
__slots__ = ("_account_url", "_basin_base_url")
10+
11+
_account_url: str
12+
_basin_base_url: str
13+
14+
def __init__(self, account_url: str, basin_base_url: str):
15+
self._account_url = account_url
16+
self._basin_base_url = basin_base_url
17+
18+
@classmethod
19+
def default(cls) -> "Endpoints":
20+
return cls(
21+
account_url="https://aws.s2.dev/v1",
22+
basin_base_url="https://{basin}.b.aws.s2.dev/v1",
23+
)
24+
25+
@classmethod
26+
@fallible
27+
def from_env(cls) -> "Endpoints":
28+
account_url = os.getenv("S2_ACCOUNT_ENDPOINT")
29+
basin_url = os.getenv("S2_BASIN_ENDPOINT")
30+
if account_url and basin_url and "{basin}" in basin_url:
31+
return cls(account_url=account_url, basin_base_url=basin_url)
32+
raise ValueError("Invalid S2_ACCOUNT_ENDPOINT and/or S2_BASIN_ENDPOINT")
33+
34+
def account(self) -> str:
35+
return self._account_url
36+
37+
def basin(self, basin_name: str) -> str:
38+
return self._basin_base_url.format(basin=basin_name)
Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,34 @@
33

44

55
class S2Error(Exception):
6-
"""
7-
Base class for all S2 related exceptions.
8-
"""
6+
"""Base class for all S2 related exceptions."""
97

108

11-
S2Error.__module__ = "streamstore"
9+
class S2ApiError(S2Error):
10+
"""Error from the S2 API."""
11+
12+
def __init__(self, message: str, status_code: int, code: str | None = None):
13+
self.status_code = status_code
14+
self.code = code
15+
super().__init__(message)
16+
17+
18+
class AppendConditionFailed(S2ApiError):
19+
"""Append condition (fencing token or seq num match) was not met."""
20+
21+
22+
class S2SessionError(S2Error):
23+
"""Error from an S2S session."""
24+
25+
def __init__(self, message: str, status_code: int):
26+
self.status_code = status_code
27+
super().__init__(message)
28+
29+
30+
S2Error.__module__ = "s2_sdk"
31+
S2ApiError.__module__ = "s2_sdk"
32+
AppendConditionFailed.__module__ = "s2_sdk"
33+
S2SessionError.__module__ = "s2_sdk"
1234

1335

1436
def fallible(f):

0 commit comments

Comments
 (0)