Skip to content

Commit e3bd2d5

Browse files
authored
Merge pull request #9 from tzafon/sync/python-sdk
sync: update Python SDK
2 parents d7ba229 + 0dc1cab commit e3bd2d5

11 files changed

Lines changed: 269 additions & 100 deletions

File tree

sdks/python/pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "tzafon"
3-
version = "2.29.2"
3+
version = "2.31.0"
44
description = "The official Python library for the lightcone API"
55
dynamic = ["readme"]
66
license = "Apache-2.0"
@@ -11,7 +11,7 @@ authors = [
1111
dependencies = [
1212
"httpx>=0.23.0, <1",
1313
"pydantic>=1.9.0, <3",
14-
"typing-extensions>=4.10, <5",
14+
"typing-extensions>=4.14, <5",
1515
"anyio>=3.5.0, <5",
1616
"distro>=1.7.0, <2",
1717
"sniffio",

sdks/python/src/tzafon/_compat.py

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
44
from datetime import date, datetime
5-
from typing_extensions import Self, Literal
5+
from typing_extensions import Self, Literal, TypedDict
66

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

133133

134+
class _ModelDumpKwargs(TypedDict, total=False):
135+
by_alias: bool
136+
137+
134138
def model_dump(
135139
model: pydantic.BaseModel,
136140
*,
@@ -142,14 +146,17 @@ def model_dump(
142146
by_alias: bool | None = None,
143147
) -> dict[str, Any]:
144148
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
149+
kwargs: _ModelDumpKwargs = {}
150+
if by_alias is not None:
151+
kwargs["by_alias"] = by_alias
145152
return model.model_dump(
146153
mode=mode,
147154
exclude=exclude,
148155
exclude_unset=exclude_unset,
149156
exclude_defaults=exclude_defaults,
150157
# warnings are not supported in Pydantic v1
151158
warnings=True if PYDANTIC_V1 else warnings,
152-
by_alias=by_alias,
159+
**kwargs,
153160
)
154161
return cast(
155162
"dict[str, Any]",

sdks/python/src/tzafon/_utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from ._path import path_template as path_template
12
from ._sync import asyncify as asyncify
23
from ._proxy import LazyProxy as LazyProxy
34
from ._utils import (
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
from __future__ import annotations
2+
3+
import re
4+
from typing import (
5+
Any,
6+
Mapping,
7+
Callable,
8+
)
9+
from urllib.parse import quote
10+
11+
# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E).
12+
_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$")
13+
14+
_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
15+
16+
17+
def _quote_path_segment_part(value: str) -> str:
18+
"""Percent-encode `value` for use in a URI path segment.
19+
20+
Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe.
21+
https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
22+
"""
23+
# quote() already treats unreserved characters (letters, digits, and -._~)
24+
# as safe, so we only need to add sub-delims, ':', and '@'.
25+
# Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted.
26+
return quote(value, safe="!$&'()*+,;=:@")
27+
28+
29+
def _quote_query_part(value: str) -> str:
30+
"""Percent-encode `value` for use in a URI query string.
31+
32+
Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe.
33+
https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
34+
"""
35+
return quote(value, safe="!$'()*+,;:@/?")
36+
37+
38+
def _quote_fragment_part(value: str) -> str:
39+
"""Percent-encode `value` for use in a URI fragment.
40+
41+
Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe.
42+
https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
43+
"""
44+
return quote(value, safe="!$&'()*+,;=:@/?")
45+
46+
47+
def _interpolate(
48+
template: str,
49+
values: Mapping[str, Any],
50+
quoter: Callable[[str], str],
51+
) -> str:
52+
"""Replace {name} placeholders in `template`, quoting each value with `quoter`.
53+
54+
Placeholder names are looked up in `values`.
55+
56+
Raises:
57+
KeyError: If a placeholder is not found in `values`.
58+
"""
59+
# re.split with a capturing group returns alternating
60+
# [text, name, text, name, ..., text] elements.
61+
parts = _PLACEHOLDER_RE.split(template)
62+
63+
for i in range(1, len(parts), 2):
64+
name = parts[i]
65+
if name not in values:
66+
raise KeyError(f"a value for placeholder {{{name}}} was not provided")
67+
val = values[name]
68+
if val is None:
69+
parts[i] = "null"
70+
elif isinstance(val, bool):
71+
parts[i] = "true" if val else "false"
72+
else:
73+
parts[i] = quoter(str(values[name]))
74+
75+
return "".join(parts)
76+
77+
78+
def path_template(template: str, /, **kwargs: Any) -> str:
79+
"""Interpolate {name} placeholders in `template` from keyword arguments.
80+
81+
Args:
82+
template: The template string containing {name} placeholders.
83+
**kwargs: Keyword arguments to interpolate into the template.
84+
85+
Returns:
86+
The template with placeholders interpolated and percent-encoded.
87+
88+
Safe characters for percent-encoding are dependent on the URI component.
89+
Placeholders in path and fragment portions are percent-encoded where the `segment`
90+
and `fragment` sets from RFC 3986 respectively are considered safe.
91+
Placeholders in the query portion are percent-encoded where the `query` set from
92+
RFC 3986 §3.3 is considered safe except for = and & characters.
93+
94+
Raises:
95+
KeyError: If a placeholder is not found in `kwargs`.
96+
ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments).
97+
"""
98+
# Split the template into path, query, and fragment portions.
99+
fragment_template: str | None = None
100+
query_template: str | None = None
101+
102+
rest = template
103+
if "#" in rest:
104+
rest, fragment_template = rest.split("#", 1)
105+
if "?" in rest:
106+
rest, query_template = rest.split("?", 1)
107+
path_template = rest
108+
109+
# Interpolate each portion with the appropriate quoting rules.
110+
path_result = _interpolate(path_template, kwargs, _quote_path_segment_part)
111+
112+
# Reject dot-segments (. and ..) in the final assembled path. The check
113+
# runs after interpolation so that adjacent placeholders or a mix of static
114+
# text and placeholders that together form a dot-segment are caught.
115+
# Also reject percent-encoded dot-segments to protect against incorrectly
116+
# implemented normalization in servers/proxies.
117+
for segment in path_result.split("/"):
118+
if _DOT_SEGMENT_RE.match(segment):
119+
raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed")
120+
121+
result = path_result
122+
if query_template is not None:
123+
result += "?" + _interpolate(query_template, kwargs, _quote_query_part)
124+
if fragment_template is not None:
125+
result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part)
126+
127+
return result

sdks/python/src/tzafon/_version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
22

33
__title__ = "tzafon"
4-
__version__ = "2.29.2" # x-release-please-version
4+
__version__ = "2.31.0" # x-release-please-version

sdks/python/src/tzafon/resources/agent/tasks.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import httpx
88

99
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
10-
from ..._utils import maybe_transform, async_maybe_transform
10+
from ..._utils import path_template, maybe_transform, async_maybe_transform
1111
from ..._compat import cached_property
1212
from ..._resource import SyncAPIResource, AsyncAPIResource
1313
from ..._response import (
@@ -76,7 +76,7 @@ def inject_message(
7676
if not id:
7777
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
7878
return self._post(
79-
f"/agent/tasks/{id}/messages",
79+
path_template("/agent/tasks/{id}/messages", id=id),
8080
body=maybe_transform({"message": message}, task_inject_message_params.TaskInjectMessageParams),
8181
options=make_request_options(
8282
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -110,7 +110,7 @@ def pause(
110110
if not id:
111111
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
112112
return self._post(
113-
f"/agent/tasks/{id}/pause",
113+
path_template("/agent/tasks/{id}/pause", id=id),
114114
options=make_request_options(
115115
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
116116
),
@@ -143,7 +143,7 @@ def resume(
143143
if not id:
144144
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
145145
return self._post(
146-
f"/agent/tasks/{id}/resume",
146+
path_template("/agent/tasks/{id}/resume", id=id),
147147
options=make_request_options(
148148
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
149149
),
@@ -176,7 +176,7 @@ def retrieve_status(
176176
if not id:
177177
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
178178
return self._get(
179-
f"/agent/tasks/{id}",
179+
path_template("/agent/tasks/{id}", id=id),
180180
options=make_request_options(
181181
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
182182
),
@@ -349,7 +349,7 @@ async def inject_message(
349349
if not id:
350350
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
351351
return await self._post(
352-
f"/agent/tasks/{id}/messages",
352+
path_template("/agent/tasks/{id}/messages", id=id),
353353
body=await async_maybe_transform({"message": message}, task_inject_message_params.TaskInjectMessageParams),
354354
options=make_request_options(
355355
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -383,7 +383,7 @@ async def pause(
383383
if not id:
384384
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
385385
return await self._post(
386-
f"/agent/tasks/{id}/pause",
386+
path_template("/agent/tasks/{id}/pause", id=id),
387387
options=make_request_options(
388388
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
389389
),
@@ -416,7 +416,7 @@ async def resume(
416416
if not id:
417417
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
418418
return await self._post(
419-
f"/agent/tasks/{id}/resume",
419+
path_template("/agent/tasks/{id}/resume", id=id),
420420
options=make_request_options(
421421
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
422422
),
@@ -449,7 +449,7 @@ async def retrieve_status(
449449
if not id:
450450
raise ValueError(f"Expected a non-empty value for `id` but received {id!r}")
451451
return await self._get(
452-
f"/agent/tasks/{id}",
452+
path_template("/agent/tasks/{id}", id=id),
453453
options=make_request_options(
454454
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
455455
),

0 commit comments

Comments
 (0)