Skip to content

Commit 2863dc7

Browse files
wanlin31copybara-github
authored andcommitted
chore: internal change
PiperOrigin-RevId: 890480489
1 parent 8a0483a commit 2863dc7

File tree

3 files changed

+147
-1
lines changed

3 files changed

+147
-1
lines changed

google/genai/_interactions/_qs.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,10 @@ def _stringify_item(
116116
items.extend(self._stringify_item(key, item, opts))
117117
return items
118118
elif array_format == "indices":
119-
raise NotImplementedError("The array indices format is not supported yet")
119+
items = []
120+
for i, item in enumerate(value):
121+
items.extend(self._stringify_item(f"{key}[{i}]", item, opts))
122+
return items
120123
elif array_format == "brackets":
121124
items = []
122125
key = key + "[]"

google/genai/_interactions/_utils/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
# limitations under the License.
1414
#
1515

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

0 commit comments

Comments
 (0)