Skip to content

Commit 0c6162b

Browse files
chore(internal): codegen related update
1 parent 2ad193d commit 0c6162b

23 files changed

Lines changed: 380 additions & 157 deletions

File tree

‎CONTRIBUTING.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ $ pip install ./path-to-wheel-file.whl
8585

8686
## Running tests
8787

88-
Most tests require you to [set up a mock server](https://github.com/stoplightio/prism) against the OpenAPI spec to run the tests.
88+
Most tests require you to [set up a mock server](https://github.com/dgellow/steady) against the OpenAPI spec to run the tests.
8989

9090
```sh
9191
$ ./scripts/mock

‎scripts/mock‎

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,34 +19,34 @@ fi
1919

2020
echo "==> Starting mock server with URL ${URL}"
2121

22-
# Run prism mock on the given spec
22+
# Run steady mock on the given spec
2323
if [ "$1" == "--daemon" ]; then
2424
# Pre-install the package so the download doesn't eat into the startup timeout
25-
npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism --version
25+
npm exec --package=@stdy/cli@0.19.3 -- steady --version
2626

27-
npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL" &> .prism.log &
27+
npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL" &> .stdy.log &
2828

29-
# Wait for server to come online (max 30s)
29+
# Wait for server to come online via health endpoint (max 30s)
3030
echo -n "Waiting for server"
3131
attempts=0
32-
while ! grep -q "✖ fatal\|Prism is listening" ".prism.log" ; do
32+
while ! curl --silent --fail "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1; do
33+
if ! kill -0 $! 2>/dev/null; then
34+
echo
35+
cat .stdy.log
36+
exit 1
37+
fi
3338
attempts=$((attempts + 1))
3439
if [ "$attempts" -ge 300 ]; then
3540
echo
36-
echo "Timed out waiting for Prism server to start"
37-
cat .prism.log
41+
echo "Timed out waiting for Steady server to start"
42+
cat .stdy.log
3843
exit 1
3944
fi
4045
echo -n "."
4146
sleep 0.1
4247
done
4348

44-
if grep -q "✖ fatal" ".prism.log"; then
45-
cat .prism.log
46-
exit 1
47-
fi
48-
4949
echo
5050
else
51-
npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock "$URL"
51+
npm exec --package=@stdy/cli@0.19.3 -- steady --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets "$URL"
5252
fi

‎scripts/test‎

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ GREEN='\033[0;32m'
99
YELLOW='\033[0;33m'
1010
NC='\033[0m' # No Color
1111

12-
function prism_is_running() {
13-
curl --silent "http://localhost:4010" >/dev/null 2>&1
12+
function steady_is_running() {
13+
curl --silent "http://127.0.0.1:4010/_x-steady/health" >/dev/null 2>&1
1414
}
1515

1616
kill_server_on_port() {
@@ -25,7 +25,7 @@ function is_overriding_api_base_url() {
2525
[ -n "$TEST_API_BASE_URL" ]
2626
}
2727

28-
if ! is_overriding_api_base_url && ! prism_is_running ; then
28+
if ! is_overriding_api_base_url && ! steady_is_running ; then
2929
# When we exit this script, make sure to kill the background mock server process
3030
trap 'kill_server_on_port 4010' EXIT
3131

@@ -36,19 +36,19 @@ fi
3636
if is_overriding_api_base_url ; then
3737
echo -e "${GREEN}✔ Running tests against ${TEST_API_BASE_URL}${NC}"
3838
echo
39-
elif ! prism_is_running ; then
40-
echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Prism server"
39+
elif ! steady_is_running ; then
40+
echo -e "${RED}ERROR:${NC} The test suite will not run without a mock Steady server"
4141
echo -e "running against your OpenAPI spec."
4242
echo
4343
echo -e "To run the server, pass in the path or url of your OpenAPI"
44-
echo -e "spec to the prism command:"
44+
echo -e "spec to the steady command:"
4545
echo
46-
echo -e " \$ ${YELLOW}npm exec --package=@stainless-api/prism-cli@5.15.0 -- prism mock path/to/your.openapi.yml${NC}"
46+
echo -e " \$ ${YELLOW}npm exec --package=@stdy/cli@0.19.3 -- steady path/to/your.openapi.yml --host 127.0.0.1 -p 4010 --validator-query-array-format=repeat --validator-query-object-format=brackets${NC}"
4747
echo
4848

4949
exit 1
5050
else
51-
echo -e "${GREEN}✔ Mock prism server is running with your OpenAPI spec${NC}"
51+
echo -e "${GREEN}✔ Mock steady server is running with your OpenAPI spec${NC}"
5252
echo
5353
fi
5454

‎src/asktable/_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 (

‎src/asktable/_utils/_path.py‎

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

‎src/asktable/resources/ats/ats.py‎

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
)
1717
from ...types import ats_list_params, ats_create_params, ats_delete_params, ats_update_params
1818
from ..._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
19-
from ..._utils import maybe_transform, async_maybe_transform
19+
from ..._utils import path_template, maybe_transform, async_maybe_transform
2020
from ..._compat import cached_property
2121
from .test_case import (
2222
TestCaseResource,
@@ -144,7 +144,7 @@ def retrieve(
144144
if not ats_id:
145145
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
146146
return self._get(
147-
f"/v1/ats/{ats_id}",
147+
path_template("/v1/ats/{ats_id}", ats_id=ats_id),
148148
options=make_request_options(
149149
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
150150
),
@@ -180,7 +180,7 @@ def update(
180180
if not ats_id:
181181
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
182182
return self._patch(
183-
f"/v1/ats/{ats_id}",
183+
path_template("/v1/ats/{ats_id}", ats_id=ats_id),
184184
body=maybe_transform({"name": name}, ats_update_params.ATSUpdateParams),
185185
options=make_request_options(
186186
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -268,7 +268,7 @@ def delete(
268268
if not ats_id:
269269
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
270270
return self._delete(
271-
f"/v1/ats/{ats_id}",
271+
path_template("/v1/ats/{ats_id}", ats_id=ats_id),
272272
options=make_request_options(
273273
extra_headers=extra_headers,
274274
extra_query=extra_query,
@@ -381,7 +381,7 @@ async def retrieve(
381381
if not ats_id:
382382
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
383383
return await self._get(
384-
f"/v1/ats/{ats_id}",
384+
path_template("/v1/ats/{ats_id}", ats_id=ats_id),
385385
options=make_request_options(
386386
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
387387
),
@@ -417,7 +417,7 @@ async def update(
417417
if not ats_id:
418418
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
419419
return await self._patch(
420-
f"/v1/ats/{ats_id}",
420+
path_template("/v1/ats/{ats_id}", ats_id=ats_id),
421421
body=await async_maybe_transform({"name": name}, ats_update_params.ATSUpdateParams),
422422
options=make_request_options(
423423
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
@@ -505,7 +505,7 @@ async def delete(
505505
if not ats_id:
506506
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
507507
return await self._delete(
508-
f"/v1/ats/{ats_id}",
508+
path_template("/v1/ats/{ats_id}", ats_id=ats_id),
509509
options=make_request_options(
510510
extra_headers=extra_headers,
511511
extra_query=extra_query,

‎src/asktable/resources/ats/task.py‎

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import httpx
66

77
from ..._types import Body, Omit, Query, Headers, NotGiven, SequenceNotStr, omit, not_given
8-
from ..._utils import maybe_transform, async_maybe_transform
8+
from ..._utils import path_template, maybe_transform, async_maybe_transform
99
from ..._compat import cached_property
1010
from ..._resource import SyncAPIResource, AsyncAPIResource
1111
from ..._response import (
@@ -76,7 +76,7 @@ def retrieve(
7676
if not ats_task_id:
7777
raise ValueError(f"Expected a non-empty value for `ats_task_id` but received {ats_task_id!r}")
7878
return self._get(
79-
f"/v1/ats/{ats_id}/task/{ats_task_id}",
79+
path_template("/v1/ats/{ats_id}/task/{ats_task_id}", ats_id=ats_id, ats_task_id=ats_task_id),
8080
options=make_request_options(
8181
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
8282
),
@@ -115,7 +115,7 @@ def list(
115115
if not ats_id:
116116
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
117117
return self._get_api_list(
118-
f"/v1/ats/{ats_id}/task",
118+
path_template("/v1/ats/{ats_id}/task", ats_id=ats_id),
119119
page=SyncPage[TaskListResponse],
120120
options=make_request_options(
121121
extra_headers=extra_headers,
@@ -168,7 +168,7 @@ def get_case_tasks(
168168
if not ats_task_id:
169169
raise ValueError(f"Expected a non-empty value for `ats_task_id` but received {ats_task_id!r}")
170170
return self._get(
171-
f"/v1/ats/{ats_id}/task/{ats_task_id}/case",
171+
path_template("/v1/ats/{ats_id}/task/{ats_task_id}/case", ats_id=ats_id, ats_task_id=ats_task_id),
172172
options=make_request_options(
173173
extra_headers=extra_headers,
174174
extra_query=extra_query,
@@ -217,7 +217,7 @@ def run(
217217
if not ats_id:
218218
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
219219
return self._post(
220-
f"/v1/ats/{ats_id}/task",
220+
path_template("/v1/ats/{ats_id}/task", ats_id=ats_id),
221221
body=maybe_transform(
222222
{
223223
"datasource_id": datasource_id,
@@ -283,7 +283,7 @@ async def retrieve(
283283
if not ats_task_id:
284284
raise ValueError(f"Expected a non-empty value for `ats_task_id` but received {ats_task_id!r}")
285285
return await self._get(
286-
f"/v1/ats/{ats_id}/task/{ats_task_id}",
286+
path_template("/v1/ats/{ats_id}/task/{ats_task_id}", ats_id=ats_id, ats_task_id=ats_task_id),
287287
options=make_request_options(
288288
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
289289
),
@@ -322,7 +322,7 @@ def list(
322322
if not ats_id:
323323
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
324324
return self._get_api_list(
325-
f"/v1/ats/{ats_id}/task",
325+
path_template("/v1/ats/{ats_id}/task", ats_id=ats_id),
326326
page=AsyncPage[TaskListResponse],
327327
options=make_request_options(
328328
extra_headers=extra_headers,
@@ -375,7 +375,7 @@ async def get_case_tasks(
375375
if not ats_task_id:
376376
raise ValueError(f"Expected a non-empty value for `ats_task_id` but received {ats_task_id!r}")
377377
return await self._get(
378-
f"/v1/ats/{ats_id}/task/{ats_task_id}/case",
378+
path_template("/v1/ats/{ats_id}/task/{ats_task_id}/case", ats_id=ats_id, ats_task_id=ats_task_id),
379379
options=make_request_options(
380380
extra_headers=extra_headers,
381381
extra_query=extra_query,
@@ -424,7 +424,7 @@ async def run(
424424
if not ats_id:
425425
raise ValueError(f"Expected a non-empty value for `ats_id` but received {ats_id!r}")
426426
return await self._post(
427-
f"/v1/ats/{ats_id}/task",
427+
path_template("/v1/ats/{ats_id}/task", ats_id=ats_id),
428428
body=await async_maybe_transform(
429429
{
430430
"datasource_id": datasource_id,

0 commit comments

Comments
 (0)