From f9b35f6d04184131168dde207f7d8dac47a7ecd7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 20 Mar 2026 03:02:11 +0000
Subject: [PATCH 01/15] fix: sanitize endpoint path params
---
src/formalize/_utils/__init__.py | 1 +
src/formalize/_utils/_path.py | 127 ++++++++++++++++++
.../resources/api/contracts/contracts.py | 6 +-
.../api/contracts/optimization_results.py | 5 +-
.../resources/api/v1/contracts/audit.py | 10 +-
tests/test_utils/test_path.py | 89 ++++++++++++
6 files changed, 228 insertions(+), 10 deletions(-)
create mode 100644 src/formalize/_utils/_path.py
create mode 100644 tests/test_utils/test_path.py
diff --git a/src/formalize/_utils/__init__.py b/src/formalize/_utils/__init__.py
index dc64e29..10cb66d 100644
--- a/src/formalize/_utils/__init__.py
+++ b/src/formalize/_utils/__init__.py
@@ -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 (
diff --git a/src/formalize/_utils/_path.py b/src/formalize/_utils/_path.py
new file mode 100644
index 0000000..4d6e1e4
--- /dev/null
+++ b/src/formalize/_utils/_path.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+import re
+from typing import (
+ Any,
+ Mapping,
+ Callable,
+)
+from urllib.parse import quote
+
+# Matches '.' or '..' where each dot is either literal or percent-encoded (%2e / %2E).
+_DOT_SEGMENT_RE = re.compile(r"^(?:\.|%2[eE]){1,2}$")
+
+_PLACEHOLDER_RE = re.compile(r"\{(\w+)\}")
+
+
+def _quote_path_segment_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI path segment.
+
+ Considers characters not in `pchar` set from RFC 3986 §3.3 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.3
+ """
+ # quote() already treats unreserved characters (letters, digits, and -._~)
+ # as safe, so we only need to add sub-delims, ':', and '@'.
+ # Notably, unlike the default `safe` for quote(), / is unsafe and must be quoted.
+ return quote(value, safe="!$&'()*+,;=:@")
+
+
+def _quote_query_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI query string.
+
+ Considers &, = and characters not in `query` set from RFC 3986 §3.4 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.4
+ """
+ return quote(value, safe="!$'()*+,;:@/?")
+
+
+def _quote_fragment_part(value: str) -> str:
+ """Percent-encode `value` for use in a URI fragment.
+
+ Considers characters not in `fragment` set from RFC 3986 §3.5 to be unsafe.
+ https://datatracker.ietf.org/doc/html/rfc3986#section-3.5
+ """
+ return quote(value, safe="!$&'()*+,;=:@/?")
+
+
+def _interpolate(
+ template: str,
+ values: Mapping[str, Any],
+ quoter: Callable[[str], str],
+) -> str:
+ """Replace {name} placeholders in `template`, quoting each value with `quoter`.
+
+ Placeholder names are looked up in `values`.
+
+ Raises:
+ KeyError: If a placeholder is not found in `values`.
+ """
+ # re.split with a capturing group returns alternating
+ # [text, name, text, name, ..., text] elements.
+ parts = _PLACEHOLDER_RE.split(template)
+
+ for i in range(1, len(parts), 2):
+ name = parts[i]
+ if name not in values:
+ raise KeyError(f"a value for placeholder {{{name}}} was not provided")
+ val = values[name]
+ if val is None:
+ parts[i] = "null"
+ elif isinstance(val, bool):
+ parts[i] = "true" if val else "false"
+ else:
+ parts[i] = quoter(str(values[name]))
+
+ return "".join(parts)
+
+
+def path_template(template: str, /, **kwargs: Any) -> str:
+ """Interpolate {name} placeholders in `template` from keyword arguments.
+
+ Args:
+ template: The template string containing {name} placeholders.
+ **kwargs: Keyword arguments to interpolate into the template.
+
+ Returns:
+ The template with placeholders interpolated and percent-encoded.
+
+ Safe characters for percent-encoding are dependent on the URI component.
+ Placeholders in path and fragment portions are percent-encoded where the `segment`
+ and `fragment` sets from RFC 3986 respectively are considered safe.
+ Placeholders in the query portion are percent-encoded where the `query` set from
+ RFC 3986 §3.3 is considered safe except for = and & characters.
+
+ Raises:
+ KeyError: If a placeholder is not found in `kwargs`.
+ ValueError: If resulting path contains /./ or /../ segments (including percent-encoded dot-segments).
+ """
+ # Split the template into path, query, and fragment portions.
+ fragment_template: str | None = None
+ query_template: str | None = None
+
+ rest = template
+ if "#" in rest:
+ rest, fragment_template = rest.split("#", 1)
+ if "?" in rest:
+ rest, query_template = rest.split("?", 1)
+ path_template = rest
+
+ # Interpolate each portion with the appropriate quoting rules.
+ path_result = _interpolate(path_template, kwargs, _quote_path_segment_part)
+
+ # Reject dot-segments (. and ..) in the final assembled path. The check
+ # runs after interpolation so that adjacent placeholders or a mix of static
+ # text and placeholders that together form a dot-segment are caught.
+ # Also reject percent-encoded dot-segments to protect against incorrectly
+ # implemented normalization in servers/proxies.
+ for segment in path_result.split("/"):
+ if _DOT_SEGMENT_RE.match(segment):
+ raise ValueError(f"Constructed path {path_result!r} contains dot-segment {segment!r} which is not allowed")
+
+ result = path_result
+ if query_template is not None:
+ result += "?" + _interpolate(query_template, kwargs, _quote_query_part)
+ if fragment_template is not None:
+ result += "#" + _interpolate(fragment_template, kwargs, _quote_fragment_part)
+
+ return result
diff --git a/src/formalize/resources/api/contracts/contracts.py b/src/formalize/resources/api/contracts/contracts.py
index 7b5075c..4f21d0c 100644
--- a/src/formalize/resources/api/contracts/contracts.py
+++ b/src/formalize/resources/api/contracts/contracts.py
@@ -7,7 +7,7 @@
import httpx
from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ...._utils import maybe_transform, async_maybe_transform
+from ...._utils import path_template, maybe_transform, async_maybe_transform
from ...._compat import cached_property
from ...._resource import SyncAPIResource, AsyncAPIResource
from ...._response import (
@@ -88,7 +88,7 @@ def optimize(
if not contract_id:
raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
return self._post(
- f"/api/contracts/{contract_id}/optimize",
+ path_template("/api/contracts/{contract_id}/optimize", contract_id=contract_id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -167,7 +167,7 @@ async def optimize(
if not contract_id:
raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
return await self._post(
- f"/api/contracts/{contract_id}/optimize",
+ path_template("/api/contracts/{contract_id}/optimize", contract_id=contract_id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
diff --git a/src/formalize/resources/api/contracts/optimization_results.py b/src/formalize/resources/api/contracts/optimization_results.py
index 3ac3d8d..85ebd71 100644
--- a/src/formalize/resources/api/contracts/optimization_results.py
+++ b/src/formalize/resources/api/contracts/optimization_results.py
@@ -5,6 +5,7 @@
import httpx
from ...._types import Body, Query, Headers, NotGiven, not_given
+from ...._utils import path_template
from ...._compat import cached_property
from ...._resource import SyncAPIResource, AsyncAPIResource
from ...._response import (
@@ -66,7 +67,7 @@ def retrieve_optimization_results(
if not contract_id:
raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
return self._get(
- f"/api/contracts/{contract_id}/optimization-results",
+ path_template("/api/contracts/{contract_id}/optimization-results", contract_id=contract_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -122,7 +123,7 @@ async def retrieve_optimization_results(
if not contract_id:
raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
return await self._get(
- f"/api/contracts/{contract_id}/optimization-results",
+ path_template("/api/contracts/{contract_id}/optimization-results", contract_id=contract_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/formalize/resources/api/v1/contracts/audit.py b/src/formalize/resources/api/v1/contracts/audit.py
index c058d61..af2cc00 100644
--- a/src/formalize/resources/api/v1/contracts/audit.py
+++ b/src/formalize/resources/api/v1/contracts/audit.py
@@ -7,7 +7,7 @@
import httpx
from ....._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ....._utils import maybe_transform, async_maybe_transform
+from ....._utils import path_template, maybe_transform, async_maybe_transform
from ....._compat import cached_property
from ....._resource import SyncAPIResource, AsyncAPIResource
from ....._response import (
@@ -81,7 +81,7 @@ def create(
if not contract_id:
raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
return self._post(
- f"/api/v1/contracts/{contract_id}/audit",
+ path_template("/api/v1/contracts/{contract_id}/audit", contract_id=contract_id),
body=maybe_transform(
{
"inputs": inputs,
@@ -127,7 +127,7 @@ def batch(
if not contract_id:
raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
return self._post(
- f"/api/v1/contracts/{contract_id}/audit/batch",
+ path_template("/api/v1/contracts/{contract_id}/audit/batch", contract_id=contract_id),
body=maybe_transform(
{
"scenarios": scenarios,
@@ -199,7 +199,7 @@ async def create(
if not contract_id:
raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
return await self._post(
- f"/api/v1/contracts/{contract_id}/audit",
+ path_template("/api/v1/contracts/{contract_id}/audit", contract_id=contract_id),
body=await async_maybe_transform(
{
"inputs": inputs,
@@ -245,7 +245,7 @@ async def batch(
if not contract_id:
raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
return await self._post(
- f"/api/v1/contracts/{contract_id}/audit/batch",
+ path_template("/api/v1/contracts/{contract_id}/audit/batch", contract_id=contract_id),
body=await async_maybe_transform(
{
"scenarios": scenarios,
diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py
new file mode 100644
index 0000000..238e309
--- /dev/null
+++ b/tests/test_utils/test_path.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+from formalize._utils._path import path_template
+
+
+@pytest.mark.parametrize(
+ "template, kwargs, expected",
+ [
+ ("/v1/{id}", dict(id="abc"), "/v1/abc"),
+ ("/v1/{a}/{b}", dict(a="x", b="y"), "/v1/x/y"),
+ ("/v1/{a}{b}/path/{c}?val={d}#{e}", dict(a="x", b="y", c="z", d="u", e="v"), "/v1/xy/path/z?val=u#v"),
+ ("/{w}/{w}", dict(w="echo"), "/echo/echo"),
+ ("/v1/static", {}, "/v1/static"),
+ ("", {}, ""),
+ ("/v1/?q={n}&count=10", dict(n=42), "/v1/?q=42&count=10"),
+ ("/v1/{v}", dict(v=None), "/v1/null"),
+ ("/v1/{v}", dict(v=True), "/v1/true"),
+ ("/v1/{v}", dict(v=False), "/v1/false"),
+ ("/v1/{v}", dict(v=".hidden"), "/v1/.hidden"), # dot prefix ok
+ ("/v1/{v}", dict(v="file.txt"), "/v1/file.txt"), # dot in middle ok
+ ("/v1/{v}", dict(v="..."), "/v1/..."), # triple dot ok
+ ("/v1/{a}{b}", dict(a=".", b="txt"), "/v1/.txt"), # dot var combining with adjacent to be ok
+ ("/items?q={v}#{f}", dict(v=".", f=".."), "/items?q=.#.."), # dots in query/fragment are fine
+ (
+ "/v1/{a}?query={b}",
+ dict(a="../../other/endpoint", b="a&bad=true"),
+ "/v1/..%2F..%2Fother%2Fendpoint?query=a%26bad%3Dtrue",
+ ),
+ ("/v1/{val}", dict(val="a/b/c"), "/v1/a%2Fb%2Fc"),
+ ("/v1/{val}", dict(val="a/b/c?query=value"), "/v1/a%2Fb%2Fc%3Fquery=value"),
+ ("/v1/{val}", dict(val="a/b/c?query=value&bad=true"), "/v1/a%2Fb%2Fc%3Fquery=value&bad=true"),
+ ("/v1/{val}", dict(val="%20"), "/v1/%2520"), # escapes escape sequences in input
+ # Query: slash and ? are safe, # is not
+ ("/items?q={v}", dict(v="a/b"), "/items?q=a/b"),
+ ("/items?q={v}", dict(v="a?b"), "/items?q=a?b"),
+ ("/items?q={v}", dict(v="a#b"), "/items?q=a%23b"),
+ ("/items?q={v}", dict(v="a b"), "/items?q=a%20b"),
+ # Fragment: slash and ? are safe
+ ("/docs#{v}", dict(v="a/b"), "/docs#a/b"),
+ ("/docs#{v}", dict(v="a?b"), "/docs#a?b"),
+ # Path: slash, ? and # are all encoded
+ ("/v1/{v}", dict(v="a/b"), "/v1/a%2Fb"),
+ ("/v1/{v}", dict(v="a?b"), "/v1/a%3Fb"),
+ ("/v1/{v}", dict(v="a#b"), "/v1/a%23b"),
+ # same var encoded differently by component
+ (
+ "/v1/{v}?q={v}#{v}",
+ dict(v="a/b?c#d"),
+ "/v1/a%2Fb%3Fc%23d?q=a/b?c%23d#a/b?c%23d",
+ ),
+ ("/v1/{val}", dict(val="x?admin=true"), "/v1/x%3Fadmin=true"), # query injection
+ ("/v1/{val}", dict(val="x#admin"), "/v1/x%23admin"), # fragment injection
+ ],
+)
+def test_interpolation(template: str, kwargs: dict[str, Any], expected: str) -> None:
+ assert path_template(template, **kwargs) == expected
+
+
+def test_missing_kwarg_raises_key_error() -> None:
+ with pytest.raises(KeyError, match="org_id"):
+ path_template("/v1/{org_id}")
+
+
+@pytest.mark.parametrize(
+ "template, kwargs",
+ [
+ ("{a}/path", dict(a=".")),
+ ("{a}/path", dict(a="..")),
+ ("/v1/{a}", dict(a=".")),
+ ("/v1/{a}", dict(a="..")),
+ ("/v1/{a}/path", dict(a=".")),
+ ("/v1/{a}/path", dict(a="..")),
+ ("/v1/{a}{b}", dict(a=".", b=".")), # adjacent vars → ".."
+ ("/v1/{a}.", dict(a=".")), # var + static → ".."
+ ("/v1/{a}{b}", dict(a="", b=".")), # empty + dot → "."
+ ("/v1/%2e/{x}", dict(x="ok")), # encoded dot in static text
+ ("/v1/%2e./{x}", dict(x="ok")), # mixed encoded ".." in static
+ ("/v1/.%2E/{x}", dict(x="ok")), # mixed encoded ".." in static
+ ("/v1/{v}?q=1", dict(v="..")),
+ ("/v1/{v}#frag", dict(v="..")),
+ ],
+)
+def test_dot_segment_rejected(template: str, kwargs: dict[str, Any]) -> None:
+ with pytest.raises(ValueError, match="dot-segment"):
+ path_template(template, **kwargs)
From 05eef18b272c2be5047fa6c51504fc1e49aca1cb Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Mar 2026 03:43:59 +0000
Subject: [PATCH 02/15] chore(internal): update gitignore
---
.gitignore | 1 +
1 file changed, 1 insertion(+)
diff --git a/.gitignore b/.gitignore
index 95ceb18..3824f4c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,4 +1,5 @@
.prism.log
+.stdy.log
_dev
__pycache__
From f3ad8e3e3f5a4f4a49bad8f60ff3202d5e487152 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Mar 2026 02:48:03 +0000
Subject: [PATCH 03/15] chore(ci): skip lint on metadata-only changes
Note that we still want to run tests, as these depend on the metadata.
---
.github/workflows/ci.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index c9bf3b9..cb8e282 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -19,7 +19,7 @@ jobs:
timeout-minutes: 10
name: lint
runs-on: ${{ github.repository == 'stainless-sdks/formalize-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
@@ -35,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:
From 563c58cb935ec2baf52614b9a2b8f7f2ac4178e7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 04:47:59 +0000
Subject: [PATCH 04/15] feat(internal): implement indices array format for
query and form serialization
---
src/formalize/_qs.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/formalize/_qs.py b/src/formalize/_qs.py
index ada6fd3..de8c99b 100644
--- a/src/formalize/_qs.py
+++ b/src/formalize/_qs.py
@@ -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 + "[]"
From 5da527973b1d2d994bf3dfeccd5e69bc65aa12f5 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 31 Mar 2026 16:47:43 +0000
Subject: [PATCH 05/15] feat: get rid of legacy routes
---
.stats.yml | 6 +-
api.md | 12 -
src/formalize/resources/api/__init__.py | 14 -
src/formalize/resources/api/api.py | 38 ---
.../resources/api/contracts/__init__.py | 33 ---
.../resources/api/contracts/contracts.py | 244 ------------------
.../api/contracts/optimization_results.py | 167 ------------
src/formalize/types/api/__init__.py | 2 -
.../types/api/contract_optimize_params.py | 20 --
tests/api_resources/api/contracts/__init__.py | 1 -
.../contracts/test_optimization_results.py | 111 --------
tests/api_resources/api/test_contracts.py | 133 ----------
12 files changed, 3 insertions(+), 778 deletions(-)
delete mode 100644 src/formalize/resources/api/contracts/__init__.py
delete mode 100644 src/formalize/resources/api/contracts/contracts.py
delete mode 100644 src/formalize/resources/api/contracts/optimization_results.py
delete mode 100644 src/formalize/types/api/contract_optimize_params.py
delete mode 100644 tests/api_resources/api/contracts/__init__.py
delete mode 100644 tests/api_resources/api/contracts/test_optimization_results.py
delete mode 100644 tests/api_resources/api/test_contracts.py
diff --git a/.stats.yml b/.stats.yml
index 0ca8a82..4a1d032 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 4
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/benchify%2Fformalize-68622707ed43c0c2934d378d9c87b746a95967ca3d2764b0889889f771317611.yml
-openapi_spec_hash: e46e13174b96c49dbca07afd1a97c2ed
+configured_endpoints: 2
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/benchify%2Fformalize-f6e0e485f1191c10775d87291f7a76776c2af882ad6a9e32af17e676dde8521e.yml
+openapi_spec_hash: b09b4f07b4f2796145d9a607d6dfa2e5
config_hash: 63fdaff946185aaff06e9fd59a007649
diff --git a/api.md b/api.md
index 2377f5b..c1b78b6 100644
--- a/api.md
+++ b/api.md
@@ -16,15 +16,3 @@ Methods:
- client.api.v1.contracts.audit.create(contract_id, \*\*params) -> AuditCreateResponse
- client.api.v1.contracts.audit.batch(contract_id, \*\*params) -> AuditBatchResponse
-
-## Contracts
-
-Methods:
-
-- client.api.contracts.optimize(contract_id, \*\*params) -> object
-
-### OptimizationResults
-
-Methods:
-
-- client.api.contracts.optimization_results.retrieve_optimization_results(contract_id) -> object
diff --git a/src/formalize/resources/api/__init__.py b/src/formalize/resources/api/__init__.py
index e241e77..c0c6efa 100644
--- a/src/formalize/resources/api/__init__.py
+++ b/src/formalize/resources/api/__init__.py
@@ -16,14 +16,6 @@
APIResourceWithStreamingResponse,
AsyncAPIResourceWithStreamingResponse,
)
-from .contracts import (
- ContractsResource,
- AsyncContractsResource,
- ContractsResourceWithRawResponse,
- AsyncContractsResourceWithRawResponse,
- ContractsResourceWithStreamingResponse,
- AsyncContractsResourceWithStreamingResponse,
-)
__all__ = [
"V1Resource",
@@ -32,12 +24,6 @@
"AsyncV1ResourceWithRawResponse",
"V1ResourceWithStreamingResponse",
"AsyncV1ResourceWithStreamingResponse",
- "ContractsResource",
- "AsyncContractsResource",
- "ContractsResourceWithRawResponse",
- "AsyncContractsResourceWithRawResponse",
- "ContractsResourceWithStreamingResponse",
- "AsyncContractsResourceWithStreamingResponse",
"APIResource",
"AsyncAPIResource",
"APIResourceWithRawResponse",
diff --git a/src/formalize/resources/api/api.py b/src/formalize/resources/api/api.py
index cde3740..8dff72a 100644
--- a/src/formalize/resources/api/api.py
+++ b/src/formalize/resources/api/api.py
@@ -12,14 +12,6 @@
AsyncV1ResourceWithStreamingResponse,
)
from ..._compat import cached_property
-from .contracts.contracts import (
- ContractsResource,
- AsyncContractsResource,
- ContractsResourceWithRawResponse,
- AsyncContractsResourceWithRawResponse,
- ContractsResourceWithStreamingResponse,
- AsyncContractsResourceWithStreamingResponse,
-)
__all__ = ["APIResource", "AsyncAPIResource"]
@@ -29,11 +21,6 @@ class APIResource(_resource.SyncAPIResource):
def v1(self) -> V1Resource:
return V1Resource(self._client)
- @cached_property
- def contracts(self) -> ContractsResource:
- """Contract optimization and AI-powered redline generation"""
- return ContractsResource(self._client)
-
@cached_property
def with_raw_response(self) -> APIResourceWithRawResponse:
"""
@@ -59,11 +46,6 @@ class AsyncAPIResource(_resource.AsyncAPIResource):
def v1(self) -> AsyncV1Resource:
return AsyncV1Resource(self._client)
- @cached_property
- def contracts(self) -> AsyncContractsResource:
- """Contract optimization and AI-powered redline generation"""
- return AsyncContractsResource(self._client)
-
@cached_property
def with_raw_response(self) -> AsyncAPIResourceWithRawResponse:
"""
@@ -92,11 +74,6 @@ def __init__(self, api: APIResource) -> None:
def v1(self) -> V1ResourceWithRawResponse:
return V1ResourceWithRawResponse(self._api.v1)
- @cached_property
- def contracts(self) -> ContractsResourceWithRawResponse:
- """Contract optimization and AI-powered redline generation"""
- return ContractsResourceWithRawResponse(self._api.contracts)
-
class AsyncAPIResourceWithRawResponse:
def __init__(self, api: AsyncAPIResource) -> None:
@@ -106,11 +83,6 @@ def __init__(self, api: AsyncAPIResource) -> None:
def v1(self) -> AsyncV1ResourceWithRawResponse:
return AsyncV1ResourceWithRawResponse(self._api.v1)
- @cached_property
- def contracts(self) -> AsyncContractsResourceWithRawResponse:
- """Contract optimization and AI-powered redline generation"""
- return AsyncContractsResourceWithRawResponse(self._api.contracts)
-
class APIResourceWithStreamingResponse:
def __init__(self, api: APIResource) -> None:
@@ -120,11 +92,6 @@ def __init__(self, api: APIResource) -> None:
def v1(self) -> V1ResourceWithStreamingResponse:
return V1ResourceWithStreamingResponse(self._api.v1)
- @cached_property
- def contracts(self) -> ContractsResourceWithStreamingResponse:
- """Contract optimization and AI-powered redline generation"""
- return ContractsResourceWithStreamingResponse(self._api.contracts)
-
class AsyncAPIResourceWithStreamingResponse:
def __init__(self, api: AsyncAPIResource) -> None:
@@ -133,8 +100,3 @@ def __init__(self, api: AsyncAPIResource) -> None:
@cached_property
def v1(self) -> AsyncV1ResourceWithStreamingResponse:
return AsyncV1ResourceWithStreamingResponse(self._api.v1)
-
- @cached_property
- def contracts(self) -> AsyncContractsResourceWithStreamingResponse:
- """Contract optimization and AI-powered redline generation"""
- return AsyncContractsResourceWithStreamingResponse(self._api.contracts)
diff --git a/src/formalize/resources/api/contracts/__init__.py b/src/formalize/resources/api/contracts/__init__.py
deleted file mode 100644
index 5c20686..0000000
--- a/src/formalize/resources/api/contracts/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from .contracts import (
- ContractsResource,
- AsyncContractsResource,
- ContractsResourceWithRawResponse,
- AsyncContractsResourceWithRawResponse,
- ContractsResourceWithStreamingResponse,
- AsyncContractsResourceWithStreamingResponse,
-)
-from .optimization_results import (
- OptimizationResultsResource,
- AsyncOptimizationResultsResource,
- OptimizationResultsResourceWithRawResponse,
- AsyncOptimizationResultsResourceWithRawResponse,
- OptimizationResultsResourceWithStreamingResponse,
- AsyncOptimizationResultsResourceWithStreamingResponse,
-)
-
-__all__ = [
- "OptimizationResultsResource",
- "AsyncOptimizationResultsResource",
- "OptimizationResultsResourceWithRawResponse",
- "AsyncOptimizationResultsResourceWithRawResponse",
- "OptimizationResultsResourceWithStreamingResponse",
- "AsyncOptimizationResultsResourceWithStreamingResponse",
- "ContractsResource",
- "AsyncContractsResource",
- "ContractsResourceWithRawResponse",
- "AsyncContractsResourceWithRawResponse",
- "ContractsResourceWithStreamingResponse",
- "AsyncContractsResourceWithStreamingResponse",
-]
diff --git a/src/formalize/resources/api/contracts/contracts.py b/src/formalize/resources/api/contracts/contracts.py
deleted file mode 100644
index 4f21d0c..0000000
--- a/src/formalize/resources/api/contracts/contracts.py
+++ /dev/null
@@ -1,244 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing import Optional
-
-import httpx
-
-from ...._types import Body, Omit, Query, Headers, NotGiven, omit, not_given
-from ...._utils import path_template, maybe_transform, async_maybe_transform
-from ...._compat import cached_property
-from ...._resource import SyncAPIResource, AsyncAPIResource
-from ...._response import (
- to_raw_response_wrapper,
- to_streamed_response_wrapper,
- async_to_raw_response_wrapper,
- async_to_streamed_response_wrapper,
-)
-from ....types.api import contract_optimize_params
-from ...._base_client import make_request_options
-from .optimization_results import (
- OptimizationResultsResource,
- AsyncOptimizationResultsResource,
- OptimizationResultsResourceWithRawResponse,
- AsyncOptimizationResultsResourceWithRawResponse,
- OptimizationResultsResourceWithStreamingResponse,
- AsyncOptimizationResultsResourceWithStreamingResponse,
-)
-
-__all__ = ["ContractsResource", "AsyncContractsResource"]
-
-
-class ContractsResource(SyncAPIResource):
- """Contract optimization and AI-powered redline generation"""
-
- @cached_property
- def optimization_results(self) -> OptimizationResultsResource:
- """Contract optimization and AI-powered redline generation"""
- return OptimizationResultsResource(self._client)
-
- @cached_property
- def with_raw_response(self) -> ContractsResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/Benchify/formalize-python#accessing-raw-response-data-eg-headers
- """
- return ContractsResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> ContractsResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/Benchify/formalize-python#with_streaming_response
- """
- return ContractsResourceWithStreamingResponse(self)
-
- def optimize(
- self,
- contract_id: str,
- *,
- analysis_concurrency: int | Omit = omit,
- num_test_inputs: int | Omit = omit,
- output_var: Optional[str] | Omit = omit,
- party_position: str | Omit = omit,
- scope_name: Optional[str] | Omit = omit,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> object:
- """
- Run Optimization
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not contract_id:
- raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
- return self._post(
- path_template("/api/contracts/{contract_id}/optimize", contract_id=contract_id),
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=maybe_transform(
- {
- "analysis_concurrency": analysis_concurrency,
- "num_test_inputs": num_test_inputs,
- "output_var": output_var,
- "party_position": party_position,
- "scope_name": scope_name,
- },
- contract_optimize_params.ContractOptimizeParams,
- ),
- ),
- cast_to=object,
- )
-
-
-class AsyncContractsResource(AsyncAPIResource):
- """Contract optimization and AI-powered redline generation"""
-
- @cached_property
- def optimization_results(self) -> AsyncOptimizationResultsResource:
- """Contract optimization and AI-powered redline generation"""
- return AsyncOptimizationResultsResource(self._client)
-
- @cached_property
- def with_raw_response(self) -> AsyncContractsResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/Benchify/formalize-python#accessing-raw-response-data-eg-headers
- """
- return AsyncContractsResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncContractsResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/Benchify/formalize-python#with_streaming_response
- """
- return AsyncContractsResourceWithStreamingResponse(self)
-
- async def optimize(
- self,
- contract_id: str,
- *,
- analysis_concurrency: int | Omit = omit,
- num_test_inputs: int | Omit = omit,
- output_var: Optional[str] | Omit = omit,
- party_position: str | Omit = omit,
- scope_name: Optional[str] | Omit = omit,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> object:
- """
- Run Optimization
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not contract_id:
- raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
- return await self._post(
- path_template("/api/contracts/{contract_id}/optimize", contract_id=contract_id),
- options=make_request_options(
- extra_headers=extra_headers,
- extra_query=extra_query,
- extra_body=extra_body,
- timeout=timeout,
- query=await async_maybe_transform(
- {
- "analysis_concurrency": analysis_concurrency,
- "num_test_inputs": num_test_inputs,
- "output_var": output_var,
- "party_position": party_position,
- "scope_name": scope_name,
- },
- contract_optimize_params.ContractOptimizeParams,
- ),
- ),
- cast_to=object,
- )
-
-
-class ContractsResourceWithRawResponse:
- def __init__(self, contracts: ContractsResource) -> None:
- self._contracts = contracts
-
- self.optimize = to_raw_response_wrapper(
- contracts.optimize,
- )
-
- @cached_property
- def optimization_results(self) -> OptimizationResultsResourceWithRawResponse:
- """Contract optimization and AI-powered redline generation"""
- return OptimizationResultsResourceWithRawResponse(self._contracts.optimization_results)
-
-
-class AsyncContractsResourceWithRawResponse:
- def __init__(self, contracts: AsyncContractsResource) -> None:
- self._contracts = contracts
-
- self.optimize = async_to_raw_response_wrapper(
- contracts.optimize,
- )
-
- @cached_property
- def optimization_results(self) -> AsyncOptimizationResultsResourceWithRawResponse:
- """Contract optimization and AI-powered redline generation"""
- return AsyncOptimizationResultsResourceWithRawResponse(self._contracts.optimization_results)
-
-
-class ContractsResourceWithStreamingResponse:
- def __init__(self, contracts: ContractsResource) -> None:
- self._contracts = contracts
-
- self.optimize = to_streamed_response_wrapper(
- contracts.optimize,
- )
-
- @cached_property
- def optimization_results(self) -> OptimizationResultsResourceWithStreamingResponse:
- """Contract optimization and AI-powered redline generation"""
- return OptimizationResultsResourceWithStreamingResponse(self._contracts.optimization_results)
-
-
-class AsyncContractsResourceWithStreamingResponse:
- def __init__(self, contracts: AsyncContractsResource) -> None:
- self._contracts = contracts
-
- self.optimize = async_to_streamed_response_wrapper(
- contracts.optimize,
- )
-
- @cached_property
- def optimization_results(self) -> AsyncOptimizationResultsResourceWithStreamingResponse:
- """Contract optimization and AI-powered redline generation"""
- return AsyncOptimizationResultsResourceWithStreamingResponse(self._contracts.optimization_results)
diff --git a/src/formalize/resources/api/contracts/optimization_results.py b/src/formalize/resources/api/contracts/optimization_results.py
deleted file mode 100644
index 85ebd71..0000000
--- a/src/formalize/resources/api/contracts/optimization_results.py
+++ /dev/null
@@ -1,167 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import httpx
-
-from ...._types import Body, Query, Headers, NotGiven, not_given
-from ...._utils import path_template
-from ...._compat import cached_property
-from ...._resource import SyncAPIResource, AsyncAPIResource
-from ...._response import (
- to_raw_response_wrapper,
- to_streamed_response_wrapper,
- async_to_raw_response_wrapper,
- async_to_streamed_response_wrapper,
-)
-from ...._base_client import make_request_options
-
-__all__ = ["OptimizationResultsResource", "AsyncOptimizationResultsResource"]
-
-
-class OptimizationResultsResource(SyncAPIResource):
- """Contract optimization and AI-powered redline generation"""
-
- @cached_property
- def with_raw_response(self) -> OptimizationResultsResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/Benchify/formalize-python#accessing-raw-response-data-eg-headers
- """
- return OptimizationResultsResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> OptimizationResultsResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/Benchify/formalize-python#with_streaming_response
- """
- return OptimizationResultsResourceWithStreamingResponse(self)
-
- def retrieve_optimization_results(
- self,
- contract_id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> object:
- """
- Get Optimization Results
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not contract_id:
- raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
- return self._get(
- path_template("/api/contracts/{contract_id}/optimization-results", contract_id=contract_id),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=object,
- )
-
-
-class AsyncOptimizationResultsResource(AsyncAPIResource):
- """Contract optimization and AI-powered redline generation"""
-
- @cached_property
- def with_raw_response(self) -> AsyncOptimizationResultsResourceWithRawResponse:
- """
- This property can be used as a prefix for any HTTP method call to return
- the raw response object instead of the parsed content.
-
- For more information, see https://www.github.com/Benchify/formalize-python#accessing-raw-response-data-eg-headers
- """
- return AsyncOptimizationResultsResourceWithRawResponse(self)
-
- @cached_property
- def with_streaming_response(self) -> AsyncOptimizationResultsResourceWithStreamingResponse:
- """
- An alternative to `.with_raw_response` that doesn't eagerly read the response body.
-
- For more information, see https://www.github.com/Benchify/formalize-python#with_streaming_response
- """
- return AsyncOptimizationResultsResourceWithStreamingResponse(self)
-
- async def retrieve_optimization_results(
- self,
- contract_id: str,
- *,
- # Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
- # The extra values given here take precedence over values defined on the client or passed to this method.
- extra_headers: Headers | None = None,
- extra_query: Query | None = None,
- extra_body: Body | None = None,
- timeout: float | httpx.Timeout | None | NotGiven = not_given,
- ) -> object:
- """
- Get Optimization Results
-
- Args:
- extra_headers: Send extra headers
-
- extra_query: Add additional query parameters to the request
-
- extra_body: Add additional JSON properties to the request
-
- timeout: Override the client-level default timeout for this request, in seconds
- """
- if not contract_id:
- raise ValueError(f"Expected a non-empty value for `contract_id` but received {contract_id!r}")
- return await self._get(
- path_template("/api/contracts/{contract_id}/optimization-results", contract_id=contract_id),
- options=make_request_options(
- extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
- ),
- cast_to=object,
- )
-
-
-class OptimizationResultsResourceWithRawResponse:
- def __init__(self, optimization_results: OptimizationResultsResource) -> None:
- self._optimization_results = optimization_results
-
- self.retrieve_optimization_results = to_raw_response_wrapper(
- optimization_results.retrieve_optimization_results,
- )
-
-
-class AsyncOptimizationResultsResourceWithRawResponse:
- def __init__(self, optimization_results: AsyncOptimizationResultsResource) -> None:
- self._optimization_results = optimization_results
-
- self.retrieve_optimization_results = async_to_raw_response_wrapper(
- optimization_results.retrieve_optimization_results,
- )
-
-
-class OptimizationResultsResourceWithStreamingResponse:
- def __init__(self, optimization_results: OptimizationResultsResource) -> None:
- self._optimization_results = optimization_results
-
- self.retrieve_optimization_results = to_streamed_response_wrapper(
- optimization_results.retrieve_optimization_results,
- )
-
-
-class AsyncOptimizationResultsResourceWithStreamingResponse:
- def __init__(self, optimization_results: AsyncOptimizationResultsResource) -> None:
- self._optimization_results = optimization_results
-
- self.retrieve_optimization_results = async_to_streamed_response_wrapper(
- optimization_results.retrieve_optimization_results,
- )
diff --git a/src/formalize/types/api/__init__.py b/src/formalize/types/api/__init__.py
index 46d0ef7..f8ee8b1 100644
--- a/src/formalize/types/api/__init__.py
+++ b/src/formalize/types/api/__init__.py
@@ -1,5 +1,3 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
from __future__ import annotations
-
-from .contract_optimize_params import ContractOptimizeParams as ContractOptimizeParams
diff --git a/src/formalize/types/api/contract_optimize_params.py b/src/formalize/types/api/contract_optimize_params.py
deleted file mode 100644
index bcce01f..0000000
--- a/src/formalize/types/api/contract_optimize_params.py
+++ /dev/null
@@ -1,20 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-from typing import Optional
-from typing_extensions import TypedDict
-
-__all__ = ["ContractOptimizeParams"]
-
-
-class ContractOptimizeParams(TypedDict, total=False):
- analysis_concurrency: int
-
- num_test_inputs: int
-
- output_var: Optional[str]
-
- party_position: str
-
- scope_name: Optional[str]
diff --git a/tests/api_resources/api/contracts/__init__.py b/tests/api_resources/api/contracts/__init__.py
deleted file mode 100644
index fd8019a..0000000
--- a/tests/api_resources/api/contracts/__init__.py
+++ /dev/null
@@ -1 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
diff --git a/tests/api_resources/api/contracts/test_optimization_results.py b/tests/api_resources/api/contracts/test_optimization_results.py
deleted file mode 100644
index 1eaa60d..0000000
--- a/tests/api_resources/api/contracts/test_optimization_results.py
+++ /dev/null
@@ -1,111 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import os
-from typing import Any, cast
-
-import pytest
-
-from formalize import Formalize, AsyncFormalize
-from tests.utils import assert_matches_type
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestOptimizationResults:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_retrieve_optimization_results(self, client: Formalize) -> None:
- optimization_result = client.api.contracts.optimization_results.retrieve_optimization_results(
- "contract_id",
- )
- assert_matches_type(object, optimization_result, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_retrieve_optimization_results(self, client: Formalize) -> None:
- response = client.api.contracts.optimization_results.with_raw_response.retrieve_optimization_results(
- "contract_id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- optimization_result = response.parse()
- assert_matches_type(object, optimization_result, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_retrieve_optimization_results(self, client: Formalize) -> None:
- with client.api.contracts.optimization_results.with_streaming_response.retrieve_optimization_results(
- "contract_id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- optimization_result = response.parse()
- assert_matches_type(object, optimization_result, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_path_params_retrieve_optimization_results(self, client: Formalize) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `contract_id` but received ''"):
- client.api.contracts.optimization_results.with_raw_response.retrieve_optimization_results(
- "",
- )
-
-
-class TestAsyncOptimizationResults:
- parametrize = pytest.mark.parametrize(
- "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
- )
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_retrieve_optimization_results(self, async_client: AsyncFormalize) -> None:
- optimization_result = await async_client.api.contracts.optimization_results.retrieve_optimization_results(
- "contract_id",
- )
- assert_matches_type(object, optimization_result, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_retrieve_optimization_results(self, async_client: AsyncFormalize) -> None:
- response = (
- await async_client.api.contracts.optimization_results.with_raw_response.retrieve_optimization_results(
- "contract_id",
- )
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- optimization_result = await response.parse()
- assert_matches_type(object, optimization_result, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_retrieve_optimization_results(self, async_client: AsyncFormalize) -> None:
- async with (
- async_client.api.contracts.optimization_results.with_streaming_response.retrieve_optimization_results(
- "contract_id",
- )
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- optimization_result = await response.parse()
- assert_matches_type(object, optimization_result, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_path_params_retrieve_optimization_results(self, async_client: AsyncFormalize) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `contract_id` but received ''"):
- await async_client.api.contracts.optimization_results.with_raw_response.retrieve_optimization_results(
- "",
- )
diff --git a/tests/api_resources/api/test_contracts.py b/tests/api_resources/api/test_contracts.py
deleted file mode 100644
index 1871abd..0000000
--- a/tests/api_resources/api/test_contracts.py
+++ /dev/null
@@ -1,133 +0,0 @@
-# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
-
-from __future__ import annotations
-
-import os
-from typing import Any, cast
-
-import pytest
-
-from formalize import Formalize, AsyncFormalize
-from tests.utils import assert_matches_type
-
-base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
-
-
-class TestContracts:
- parametrize = pytest.mark.parametrize("client", [False, True], indirect=True, ids=["loose", "strict"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_optimize(self, client: Formalize) -> None:
- contract = client.api.contracts.optimize(
- contract_id="contract_id",
- )
- assert_matches_type(object, contract, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_method_optimize_with_all_params(self, client: Formalize) -> None:
- contract = client.api.contracts.optimize(
- contract_id="contract_id",
- analysis_concurrency=0,
- num_test_inputs=0,
- output_var="output_var",
- party_position="party_position",
- scope_name="scope_name",
- )
- assert_matches_type(object, contract, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_raw_response_optimize(self, client: Formalize) -> None:
- response = client.api.contracts.with_raw_response.optimize(
- contract_id="contract_id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- contract = response.parse()
- assert_matches_type(object, contract, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_streaming_response_optimize(self, client: Formalize) -> None:
- with client.api.contracts.with_streaming_response.optimize(
- contract_id="contract_id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- contract = response.parse()
- assert_matches_type(object, contract, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- def test_path_params_optimize(self, client: Formalize) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `contract_id` but received ''"):
- client.api.contracts.with_raw_response.optimize(
- contract_id="",
- )
-
-
-class TestAsyncContracts:
- parametrize = pytest.mark.parametrize(
- "async_client", [False, True, {"http_client": "aiohttp"}], indirect=True, ids=["loose", "strict", "aiohttp"]
- )
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_optimize(self, async_client: AsyncFormalize) -> None:
- contract = await async_client.api.contracts.optimize(
- contract_id="contract_id",
- )
- assert_matches_type(object, contract, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_method_optimize_with_all_params(self, async_client: AsyncFormalize) -> None:
- contract = await async_client.api.contracts.optimize(
- contract_id="contract_id",
- analysis_concurrency=0,
- num_test_inputs=0,
- output_var="output_var",
- party_position="party_position",
- scope_name="scope_name",
- )
- assert_matches_type(object, contract, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_raw_response_optimize(self, async_client: AsyncFormalize) -> None:
- response = await async_client.api.contracts.with_raw_response.optimize(
- contract_id="contract_id",
- )
-
- assert response.is_closed is True
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
- contract = await response.parse()
- assert_matches_type(object, contract, path=["response"])
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_streaming_response_optimize(self, async_client: AsyncFormalize) -> None:
- async with async_client.api.contracts.with_streaming_response.optimize(
- contract_id="contract_id",
- ) as response:
- assert not response.is_closed
- assert response.http_request.headers.get("X-Stainless-Lang") == "python"
-
- contract = await response.parse()
- assert_matches_type(object, contract, path=["response"])
-
- assert cast(Any, response.is_closed) is True
-
- @pytest.mark.skip(reason="Mock server tests are disabled")
- @parametrize
- async def test_path_params_optimize(self, async_client: AsyncFormalize) -> None:
- with pytest.raises(ValueError, match=r"Expected a non-empty value for `contract_id` but received ''"):
- await async_client.api.contracts.with_raw_response.optimize(
- contract_id="",
- )
From cb1116e13d34679fa2cd73fe17cbf3bfa74c106b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 8 Apr 2026 04:01:01 +0000
Subject: [PATCH 06/15] fix(client): preserve hardcoded query params when
merging with user params
---
src/formalize/_base_client.py | 4 +++
tests/test_client.py | 48 +++++++++++++++++++++++++++++++++++
2 files changed, 52 insertions(+)
diff --git a/src/formalize/_base_client.py b/src/formalize/_base_client.py
index 311a205..00ad8b5 100644
--- a/src/formalize/_base_client.py
+++ b/src/formalize/_base_client.py
@@ -558,6 +558,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("_", "-")}
diff --git a/tests/test_client.py b/tests/test_client.py
index d254784..d4e6938 100644
--- a/tests/test_client.py
+++ b/tests/test_client.py
@@ -436,6 +436,30 @@ def test_default_query_option(self) -> None:
client.close()
+ def test_hardcoded_query_params_in_url(self, client: Formalize) -> None:
+ request = client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true"))
+ url = httpx.URL(request.url)
+ assert dict(url.params) == {"beta": "true"}
+
+ request = client._build_request(
+ FinalRequestOptions(
+ method="get",
+ url="/foo?beta=true",
+ params={"limit": "10", "page": "abc"},
+ )
+ )
+ url = httpx.URL(request.url)
+ assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"}
+
+ request = client._build_request(
+ FinalRequestOptions(
+ method="get",
+ url="/files/a%2Fb?beta=true",
+ params={"limit": "10"},
+ )
+ )
+ assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10"
+
def test_request_extra_json(self, client: Formalize) -> None:
request = client._build_request(
FinalRequestOptions(
@@ -1368,6 +1392,30 @@ async def test_default_query_option(self) -> None:
await client.close()
+ async def test_hardcoded_query_params_in_url(self, async_client: AsyncFormalize) -> None:
+ request = async_client._build_request(FinalRequestOptions(method="get", url="/foo?beta=true"))
+ url = httpx.URL(request.url)
+ assert dict(url.params) == {"beta": "true"}
+
+ request = async_client._build_request(
+ FinalRequestOptions(
+ method="get",
+ url="/foo?beta=true",
+ params={"limit": "10", "page": "abc"},
+ )
+ )
+ url = httpx.URL(request.url)
+ assert dict(url.params) == {"beta": "true", "limit": "10", "page": "abc"}
+
+ request = async_client._build_request(
+ FinalRequestOptions(
+ method="get",
+ url="/files/a%2Fb?beta=true",
+ params={"limit": "10"},
+ )
+ )
+ assert request.url.raw_path == b"/files/a%2Fb?beta=true&limit=10"
+
def test_request_extra_json(self, client: Formalize) -> None:
request = client._build_request(
FinalRequestOptions(
From f2e61d8e07e220de2b785c0b9985e403bf97fca1 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 11 Apr 2026 06:30:54 +0000
Subject: [PATCH 07/15] fix: ensure file data are only sent as 1 parameter
---
src/formalize/_utils/_utils.py | 5 +++--
tests/test_extract_files.py | 9 +++++++++
2 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/src/formalize/_utils/_utils.py b/src/formalize/_utils/_utils.py
index eec7f4a..63b8cd6 100644
--- a/src/formalize/_utils/_utils.py
+++ b/src/formalize/_utils/_utils.py
@@ -86,8 +86,9 @@ def _extract_items(
index += 1
if is_dict(obj):
try:
- # We are at the last entry in the path so we must remove the field
- if (len(path)) == index:
+ # Remove the field if there are no more dict keys in the path,
+ # only "" traversal markers or end.
+ if all(p == "" for p in path[index:]):
item = obj.pop(key)
else:
item = obj[key]
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index 48510b1..730d53d 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -35,6 +35,15 @@ def test_multiple_files() -> None:
assert query == {"documents": [{}, {}]}
+def test_top_level_file_array() -> None:
+ query = {"files": [b"file one", b"file two"], "title": "hello"}
+ assert extract_files(query, paths=[["files", ""]]) == [
+ ("files[]", b"file one"),
+ ("files[]", b"file two"),
+ ]
+ assert query == {"title": "hello"}
+
+
@pytest.mark.parametrize(
"query,paths,expected",
[
From aa8ebe4e973bdabed6e614b86ea7662918925955 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sat, 18 Apr 2026 06:10:59 +0000
Subject: [PATCH 08/15] perf(client): optimize file structure copying in
multipart requests
---
src/formalize/_files.py | 56 +++++++++++++++++-
src/formalize/_utils/__init__.py | 1 -
src/formalize/_utils/_utils.py | 15 -----
tests/test_deepcopy.py | 58 -------------------
tests/test_files.py | 99 +++++++++++++++++++++++++++++++-
5 files changed, 151 insertions(+), 78 deletions(-)
delete mode 100644 tests/test_deepcopy.py
diff --git a/src/formalize/_files.py b/src/formalize/_files.py
index cc14c14..0fdce17 100644
--- a/src/formalize/_files.py
+++ b/src/formalize/_files.py
@@ -3,8 +3,8 @@
import io
import os
import pathlib
-from typing import overload
-from typing_extensions import TypeGuard
+from typing import Sequence, cast, overload
+from typing_extensions import TypeVar, TypeGuard
import anyio
@@ -17,7 +17,9 @@
HttpxFileContent,
HttpxRequestFiles,
)
-from ._utils import is_tuple_t, is_mapping_t, is_sequence_t
+from ._utils import is_list, is_mapping, is_tuple_t, is_mapping_t, is_sequence_t
+
+_T = TypeVar("_T")
def is_base64_file_input(obj: object) -> TypeGuard[Base64FileInput]:
@@ -121,3 +123,51 @@ async def async_read_file_content(file: FileContent) -> HttpxFileContent:
return await anyio.Path(file).read_bytes()
return file
+
+
+def deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]]) -> _T:
+ """Copy only the containers along the given paths.
+
+ Used to guard against mutation by extract_files without copying the entire structure.
+ Only dicts and lists that lie on a path are copied; everything else
+ is returned by reference.
+
+ For example, given paths=[["foo", "files", "file"]] and the structure:
+ {
+ "foo": {
+ "bar": {"baz": {}},
+ "files": {"file": }
+ }
+ }
+ The root dict, "foo", and "files" are copied (they lie on the path).
+ "bar" and "baz" are returned by reference (off the path).
+ """
+ return _deepcopy_with_paths(item, paths, 0)
+
+
+def _deepcopy_with_paths(item: _T, paths: Sequence[Sequence[str]], index: int) -> _T:
+ if not paths:
+ return item
+ if is_mapping(item):
+ key_to_paths: dict[str, list[Sequence[str]]] = {}
+ for path in paths:
+ if index < len(path):
+ key_to_paths.setdefault(path[index], []).append(path)
+
+ # if no path continues through this mapping, it won't be mutated and copying it is redundant
+ if not key_to_paths:
+ return item
+
+ result = dict(item)
+ for key, subpaths in key_to_paths.items():
+ if key in result:
+ result[key] = _deepcopy_with_paths(result[key], subpaths, index + 1)
+ return cast(_T, result)
+ if is_list(item):
+ array_paths = [path for path in paths if index < len(path) and path[index] == ""]
+
+ # if no path expects a list here, nothing will be mutated inside it - return by reference
+ if not array_paths:
+ return cast(_T, item)
+ return cast(_T, [_deepcopy_with_paths(entry, array_paths, index + 1) for entry in item])
+ return item
diff --git a/src/formalize/_utils/__init__.py b/src/formalize/_utils/__init__.py
index 10cb66d..1c090e5 100644
--- a/src/formalize/_utils/__init__.py
+++ b/src/formalize/_utils/__init__.py
@@ -24,7 +24,6 @@
coerce_integer as coerce_integer,
file_from_path as file_from_path,
strip_not_given as strip_not_given,
- deepcopy_minimal as deepcopy_minimal,
get_async_library as get_async_library,
maybe_coerce_float as maybe_coerce_float,
get_required_header as get_required_header,
diff --git a/src/formalize/_utils/_utils.py b/src/formalize/_utils/_utils.py
index 63b8cd6..771859f 100644
--- a/src/formalize/_utils/_utils.py
+++ b/src/formalize/_utils/_utils.py
@@ -177,21 +177,6 @@ def is_iterable(obj: object) -> TypeGuard[Iterable[object]]:
return isinstance(obj, Iterable)
-def deepcopy_minimal(item: _T) -> _T:
- """Minimal reimplementation of copy.deepcopy() that will only copy certain object types:
-
- - mappings, e.g. `dict`
- - list
-
- This is done for performance reasons.
- """
- if is_mapping(item):
- return cast(_T, {k: deepcopy_minimal(v) for k, v in item.items()})
- if is_list(item):
- return cast(_T, [deepcopy_minimal(entry) for entry in item])
- return item
-
-
# copied from https://github.com/Rapptz/RoboDanny
def human_join(seq: Sequence[str], *, delim: str = ", ", final: str = "or") -> str:
size = len(seq)
diff --git a/tests/test_deepcopy.py b/tests/test_deepcopy.py
deleted file mode 100644
index 544a157..0000000
--- a/tests/test_deepcopy.py
+++ /dev/null
@@ -1,58 +0,0 @@
-from formalize._utils import deepcopy_minimal
-
-
-def assert_different_identities(obj1: object, obj2: object) -> None:
- assert obj1 == obj2
- assert id(obj1) != id(obj2)
-
-
-def test_simple_dict() -> None:
- obj1 = {"foo": "bar"}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_dict() -> None:
- obj1 = {"foo": {"bar": True}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
-
-
-def test_complex_nested_dict() -> None:
- obj1 = {"foo": {"bar": [{"hello": "world"}]}}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1["foo"], obj2["foo"])
- assert_different_identities(obj1["foo"]["bar"], obj2["foo"]["bar"])
- assert_different_identities(obj1["foo"]["bar"][0], obj2["foo"]["bar"][0])
-
-
-def test_simple_list() -> None:
- obj1 = ["a", "b", "c"]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
-
-
-def test_nested_list() -> None:
- obj1 = ["a", [1, 2, 3]]
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert_different_identities(obj1[1], obj2[1])
-
-
-class MyObject: ...
-
-
-def test_ignores_other_types() -> None:
- # custom classes
- my_obj = MyObject()
- obj1 = {"foo": my_obj}
- obj2 = deepcopy_minimal(obj1)
- assert_different_identities(obj1, obj2)
- assert obj1["foo"] is my_obj
-
- # tuples
- obj3 = ("a", "b")
- obj4 = deepcopy_minimal(obj3)
- assert obj3 is obj4
diff --git a/tests/test_files.py b/tests/test_files.py
index 8c91748..ab9241a 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -4,7 +4,8 @@
import pytest
from dirty_equals import IsDict, IsList, IsBytes, IsTuple
-from formalize._files import to_httpx_files, async_to_httpx_files
+from formalize._files import to_httpx_files, deepcopy_with_paths, async_to_httpx_files
+from formalize._utils import extract_files
readme_path = Path(__file__).parent.parent.joinpath("README.md")
@@ -49,3 +50,99 @@ def test_string_not_allowed() -> None:
"file": "foo", # type: ignore
}
)
+
+
+def assert_different_identities(obj1: object, obj2: object) -> None:
+ assert obj1 == obj2
+ assert obj1 is not obj2
+
+
+class TestDeepcopyWithPaths:
+ def test_copies_top_level_dict(self) -> None:
+ original = {"file": b"data", "other": "value"}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+
+ def test_file_value_is_same_reference(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes}
+ result = deepcopy_with_paths(original, [["file"]])
+ assert_different_identities(result, original)
+ assert result["file"] is file_bytes
+
+ def test_list_popped_wholesale(self) -> None:
+ files = [b"f1", b"f2"]
+ original = {"files": files, "title": "t"}
+ result = deepcopy_with_paths(original, [["files", ""]])
+ assert_different_identities(result, original)
+ result_files = result["files"]
+ assert isinstance(result_files, list)
+ assert_different_identities(result_files, files)
+
+ def test_nested_array_path_copies_list_and_elements(self) -> None:
+ elem1 = {"file": b"f1", "extra": 1}
+ elem2 = {"file": b"f2", "extra": 2}
+ original = {"items": [elem1, elem2]}
+ result = deepcopy_with_paths(original, [["items", "", "file"]])
+ assert_different_identities(result, original)
+ result_items = result["items"]
+ assert isinstance(result_items, list)
+ assert_different_identities(result_items, original["items"])
+ assert_different_identities(result_items[0], elem1)
+ assert_different_identities(result_items[1], elem2)
+
+ def test_empty_paths_returns_same_object(self) -> None:
+ original = {"foo": "bar"}
+ result = deepcopy_with_paths(original, [])
+ assert result is original
+
+ def test_multiple_paths(self) -> None:
+ f1 = b"file1"
+ f2 = b"file2"
+ original = {"a": f1, "b": f2, "c": "unchanged"}
+ result = deepcopy_with_paths(original, [["a"], ["b"]])
+ assert_different_identities(result, original)
+ assert result["a"] is f1
+ assert result["b"] is f2
+ assert result["c"] is original["c"]
+
+ def test_extract_files_does_not_mutate_original_top_level(self) -> None:
+ file_bytes = b"contents"
+ original = {"file": file_bytes, "other": "value"}
+
+ copied = deepcopy_with_paths(original, [["file"]])
+ extracted = extract_files(copied, paths=[["file"]])
+
+ assert extracted == [("file", file_bytes)]
+ assert original == {"file": file_bytes, "other": "value"}
+ assert copied == {"other": "value"}
+
+ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None:
+ file1 = b"f1"
+ file2 = b"f2"
+ original = {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+
+ copied = deepcopy_with_paths(original, [["items", "", "file"]])
+ extracted = extract_files(copied, paths=[["items", "", "file"]])
+
+ assert extracted == [("items[][file]", file1), ("items[][file]", file2)]
+ assert original == {
+ "items": [
+ {"file": file1, "extra": 1},
+ {"file": file2, "extra": 2},
+ ],
+ "title": "example",
+ }
+ assert copied == {
+ "items": [
+ {"extra": 1},
+ {"extra": 2},
+ ],
+ "title": "example",
+ }
From f3e33d5e7b7652b0764ae2f39ca186aaba690a4c Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 23 Apr 2026 03:24:51 +0000
Subject: [PATCH 09/15] chore(internal): more robust bootstrap script
---
scripts/bootstrap | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/scripts/bootstrap b/scripts/bootstrap
index 4638ec6..5a23841 100755
--- a/scripts/bootstrap
+++ b/scripts/bootstrap
@@ -4,7 +4,7 @@ set -e
cd "$(dirname "$0")/.."
-if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "$SKIP_BREW" != "1" ] && [ -t 0 ]; then
+if [ -f "Brewfile" ] && [ "$(uname -s)" = "Darwin" ] && [ "${SKIP_BREW:-}" != "1" ] && [ -t 0 ]; then
brew bundle check >/dev/null 2>&1 || {
echo -n "==> Install Homebrew dependencies? (y/N): "
read -r response
From 66306915b66d66bc80074de143ce101ba7f3c3c0 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:18:35 +0000
Subject: [PATCH 10/15] fix: use correct field name format for multipart file
arrays
---
src/formalize/_qs.py | 8 ++-----
src/formalize/_types.py | 3 +++
src/formalize/_utils/_utils.py | 42 +++++++++++++++++++++++++++-------
tests/test_extract_files.py | 28 +++++++++++++++++++----
tests/test_files.py | 2 +-
5 files changed, 63 insertions(+), 20 deletions(-)
diff --git a/src/formalize/_qs.py b/src/formalize/_qs.py
index de8c99b..4127c19 100644
--- a/src/formalize/_qs.py
+++ b/src/formalize/_qs.py
@@ -2,17 +2,13 @@
from typing import Any, List, Tuple, Union, Mapping, TypeVar
from urllib.parse import parse_qs, urlencode
-from typing_extensions import Literal, get_args
+from typing_extensions import get_args
-from ._types import NotGiven, not_given
+from ._types import NotGiven, ArrayFormat, NestedFormat, not_given
from ._utils import flatten
_T = TypeVar("_T")
-
-ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
-NestedFormat = Literal["dots", "brackets"]
-
PrimitiveData = Union[str, int, float, bool, None]
# this should be Data = Union[PrimitiveData, "List[Data]", "Tuple[Data]", "Mapping[str, Data]"]
# https://github.com/microsoft/pyright/issues/3555
diff --git a/src/formalize/_types.py b/src/formalize/_types.py
index dbc2924..2455747 100644
--- a/src/formalize/_types.py
+++ b/src/formalize/_types.py
@@ -47,6 +47,9 @@
ModelT = TypeVar("ModelT", bound=pydantic.BaseModel)
_T = TypeVar("_T")
+ArrayFormat = Literal["comma", "repeat", "indices", "brackets"]
+NestedFormat = Literal["dots", "brackets"]
+
# Approximates httpx internal ProxiesTypes and RequestFiles types
# while adding support for `PathLike` instances
diff --git a/src/formalize/_utils/_utils.py b/src/formalize/_utils/_utils.py
index 771859f..199cd23 100644
--- a/src/formalize/_utils/_utils.py
+++ b/src/formalize/_utils/_utils.py
@@ -17,11 +17,11 @@
)
from pathlib import Path
from datetime import date, datetime
-from typing_extensions import TypeGuard
+from typing_extensions import TypeGuard, get_args
import sniffio
-from .._types import Omit, NotGiven, FileTypes, HeadersLike
+from .._types import Omit, NotGiven, FileTypes, ArrayFormat, HeadersLike
_T = TypeVar("_T")
_TupleT = TypeVar("_TupleT", bound=Tuple[object, ...])
@@ -40,25 +40,45 @@ def extract_files(
query: Mapping[str, object],
*,
paths: Sequence[Sequence[str]],
+ array_format: ArrayFormat = "brackets",
) -> list[tuple[str, FileTypes]]:
"""Recursively extract files from the given dictionary based on specified paths.
A path may look like this ['foo', 'files', '', 'data'].
+ ``array_format`` controls how ```` segments contribute to the emitted
+ field name. Supported values: ``"brackets"`` (``foo[]``), ``"repeat"`` and
+ ``"comma"`` (``foo``), ``"indices"`` (``foo[0]``, ``foo[1]``).
+
Note: this mutates the given dictionary.
"""
files: list[tuple[str, FileTypes]] = []
for path in paths:
- files.extend(_extract_items(query, path, index=0, flattened_key=None))
+ files.extend(_extract_items(query, path, index=0, flattened_key=None, array_format=array_format))
return files
+def _array_suffix(array_format: ArrayFormat, array_index: int) -> str:
+ if array_format == "brackets":
+ return "[]"
+ if array_format == "indices":
+ return f"[{array_index}]"
+ if array_format == "repeat" or array_format == "comma":
+ # Both repeat the bare field name for each file part; there is no
+ # meaningful way to comma-join binary parts.
+ return ""
+ raise NotImplementedError(
+ f"Unknown array_format value: {array_format}, choose from {', '.join(get_args(ArrayFormat))}"
+ )
+
+
def _extract_items(
obj: object,
path: Sequence[str],
*,
index: int,
flattened_key: str | None,
+ array_format: ArrayFormat,
) -> list[tuple[str, FileTypes]]:
try:
key = path[index]
@@ -75,9 +95,11 @@ def _extract_items(
if is_list(obj):
files: list[tuple[str, FileTypes]] = []
- for entry in obj:
- assert_is_file_content(entry, key=flattened_key + "[]" if flattened_key else "")
- files.append((flattened_key + "[]", cast(FileTypes, entry)))
+ for array_index, entry in enumerate(obj):
+ suffix = _array_suffix(array_format, array_index)
+ emitted_key = (flattened_key + suffix) if flattened_key else suffix
+ assert_is_file_content(entry, key=emitted_key)
+ files.append((emitted_key, cast(FileTypes, entry)))
return files
assert_is_file_content(obj, key=flattened_key)
@@ -106,6 +128,7 @@ def _extract_items(
path,
index=index,
flattened_key=flattened_key,
+ array_format=array_format,
)
elif is_list(obj):
if key != "":
@@ -117,9 +140,12 @@ def _extract_items(
item,
path,
index=index,
- flattened_key=flattened_key + "[]" if flattened_key is not None else "[]",
+ flattened_key=(
+ (flattened_key if flattened_key is not None else "") + _array_suffix(array_format, array_index)
+ ),
+ array_format=array_format,
)
- for item in obj
+ for array_index, item in enumerate(obj)
]
)
diff --git a/tests/test_extract_files.py b/tests/test_extract_files.py
index 730d53d..c0f97fc 100644
--- a/tests/test_extract_files.py
+++ b/tests/test_extract_files.py
@@ -4,7 +4,7 @@
import pytest
-from formalize._types import FileTypes
+from formalize._types import FileTypes, ArrayFormat
from formalize._utils import extract_files
@@ -37,10 +37,7 @@ def test_multiple_files() -> None:
def test_top_level_file_array() -> None:
query = {"files": [b"file one", b"file two"], "title": "hello"}
- assert extract_files(query, paths=[["files", ""]]) == [
- ("files[]", b"file one"),
- ("files[]", b"file two"),
- ]
+ assert extract_files(query, paths=[["files", ""]]) == [("files[]", b"file one"), ("files[]", b"file two")]
assert query == {"title": "hello"}
@@ -71,3 +68,24 @@ def test_ignores_incorrect_paths(
expected: list[tuple[str, FileTypes]],
) -> None:
assert extract_files(query, paths=paths) == expected
+
+
+@pytest.mark.parametrize(
+ "array_format,expected_top_level,expected_nested",
+ [
+ ("brackets", [("files[]", b"a"), ("files[]", b"b")], [("items[][file]", b"a"), ("items[][file]", b"b")]),
+ ("repeat", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("comma", [("files", b"a"), ("files", b"b")], [("items[file]", b"a"), ("items[file]", b"b")]),
+ ("indices", [("files[0]", b"a"), ("files[1]", b"b")], [("items[0][file]", b"a"), ("items[1][file]", b"b")]),
+ ],
+)
+def test_array_format_controls_file_field_names(
+ array_format: ArrayFormat,
+ expected_top_level: list[tuple[str, FileTypes]],
+ expected_nested: list[tuple[str, FileTypes]],
+) -> None:
+ top_level = {"files": [b"a", b"b"]}
+ assert extract_files(top_level, paths=[["files", ""]], array_format=array_format) == expected_top_level
+
+ nested = {"items": [{"file": b"a"}, {"file": b"b"}]}
+ assert extract_files(nested, paths=[["items", "", "file"]], array_format=array_format) == expected_nested
diff --git a/tests/test_files.py b/tests/test_files.py
index ab9241a..8ef02b5 100644
--- a/tests/test_files.py
+++ b/tests/test_files.py
@@ -131,7 +131,7 @@ def test_extract_files_does_not_mutate_original_nested_array_path(self) -> None:
copied = deepcopy_with_paths(original, [["items", "", "file"]])
extracted = extract_files(copied, paths=[["items", "", "file"]])
- assert extracted == [("items[][file]", file1), ("items[][file]", file2)]
+ assert [entry for _, entry in extracted] == [file1, file2]
assert original == {
"items": [
{"file": file1, "extra": 1},
From 799fdc05dfafc29f30f553861ec6ee524410b20a Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 28 Apr 2026 03:20:54 +0000
Subject: [PATCH 11/15] feat: support setting headers via env
---
src/formalize/_client.py | 24 +++++++++++++++++++++++-
1 file changed, 23 insertions(+), 1 deletion(-)
diff --git a/src/formalize/_client.py b/src/formalize/_client.py
index af485be..ea2aee4 100644
--- a/src/formalize/_client.py
+++ b/src/formalize/_client.py
@@ -20,7 +20,11 @@
RequestOptions,
not_given,
)
-from ._utils import is_given, get_async_library
+from ._utils import (
+ is_given,
+ is_mapping_t,
+ get_async_library,
+)
from ._compat import cached_property
from ._models import SecurityOptions
from ._version import __version__
@@ -96,6 +100,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.example.com"
+ custom_headers_env = os.environ.get("FORMALIZE_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
@@ -303,6 +316,15 @@ def __init__(
if base_url is None:
base_url = f"https://api.example.com"
+ custom_headers_env = os.environ.get("FORMALIZE_CUSTOM_HEADERS")
+ if custom_headers_env is not None:
+ parsed: dict[str, str] = {}
+ for line in custom_headers_env.split("\n"):
+ colon = line.find(":")
+ if colon >= 0:
+ parsed[line[:colon].strip()] = line[colon + 1 :].strip()
+ default_headers = {**parsed, **(default_headers if is_mapping_t(default_headers) else {})}
+
super().__init__(
version=__version__,
base_url=base_url,
From 0c343ad538c6b2f1dd3af5a38ee481fbe635997b Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 30 Apr 2026 05:28:06 +0000
Subject: [PATCH 12/15] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 4a1d032..13dc221 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 2
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/benchify%2Fformalize-f6e0e485f1191c10775d87291f7a76776c2af882ad6a9e32af17e676dde8521e.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/benchify/formalize-f6e0e485f1191c10775d87291f7a76776c2af882ad6a9e32af17e676dde8521e.yml
openapi_spec_hash: b09b4f07b4f2796145d9a607d6dfa2e5
config_hash: 63fdaff946185aaff06e9fd59a007649
From 0d9af4fe65c703c63ab0caada2e4461446ca3cb8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 04:10:15 +0000
Subject: [PATCH 13/15] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 13dc221..264edbd 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 2
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/benchify/formalize-f6e0e485f1191c10775d87291f7a76776c2af882ad6a9e32af17e676dde8521e.yml
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/benchify/formalize-3c52b5aeafb4d26b3650c62776527aecbb19df133407809a00acdf8b365e4ceb.yml
openapi_spec_hash: b09b4f07b4f2796145d9a607d6dfa2e5
config_hash: 63fdaff946185aaff06e9fd59a007649
From cc326cfca752a635b78a66cc326fae8b58f62ad4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 04:14:31 +0000
Subject: [PATCH 14/15] chore(internal): reformat pyproject.toml
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index aca0d37..0eb2b6c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -154,7 +154,7 @@ show_error_codes = true
#
# We also exclude our `tests` as mypy doesn't always infer
# types correctly and Pyright will still catch any type errors.
-exclude = ['src/formalize/_files.py', '_dev/.*.py', 'tests/.*']
+exclude = ["src/formalize/_files.py", "_dev/.*.py", "tests/.*"]
strict_equality = true
implicit_reexport = true
From 9488e675f23f8afd95d4853575d3afbcffa055f6 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 1 May 2026 04:16:54 +0000
Subject: [PATCH 15/15] release: 1.4.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 31 +++++++++++++++++++++++++++++++
pyproject.toml | 2 +-
src/formalize/_version.py | 2 +-
4 files changed, 34 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 2a8f4ff..3e9af1b 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "1.3.0"
+ ".": "1.4.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 18f9952..f696857 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,36 @@
# Changelog
+## 1.4.0 (2026-05-01)
+
+Full Changelog: [v1.3.0...v1.4.0](https://github.com/Benchify/formalize-python/compare/v1.3.0...v1.4.0)
+
+### Features
+
+* get rid of legacy routes ([5da5279](https://github.com/Benchify/formalize-python/commit/5da527973b1d2d994bf3dfeccd5e69bc65aa12f5))
+* **internal:** implement indices array format for query and form serialization ([563c58c](https://github.com/Benchify/formalize-python/commit/563c58cb935ec2baf52614b9a2b8f7f2ac4178e7))
+* support setting headers via env ([799fdc0](https://github.com/Benchify/formalize-python/commit/799fdc05dfafc29f30f553861ec6ee524410b20a))
+
+
+### Bug Fixes
+
+* **client:** preserve hardcoded query params when merging with user params ([cb1116e](https://github.com/Benchify/formalize-python/commit/cb1116e13d34679fa2cd73fe17cbf3bfa74c106b))
+* ensure file data are only sent as 1 parameter ([f2e61d8](https://github.com/Benchify/formalize-python/commit/f2e61d8e07e220de2b785c0b9985e403bf97fca1))
+* sanitize endpoint path params ([f9b35f6](https://github.com/Benchify/formalize-python/commit/f9b35f6d04184131168dde207f7d8dac47a7ecd7))
+* use correct field name format for multipart file arrays ([6630691](https://github.com/Benchify/formalize-python/commit/66306915b66d66bc80074de143ce101ba7f3c3c0))
+
+
+### Performance Improvements
+
+* **client:** optimize file structure copying in multipart requests ([aa8ebe4](https://github.com/Benchify/formalize-python/commit/aa8ebe4e973bdabed6e614b86ea7662918925955))
+
+
+### Chores
+
+* **ci:** skip lint on metadata-only changes ([f3ad8e3](https://github.com/Benchify/formalize-python/commit/f3ad8e3e3f5a4f4a49bad8f60ff3202d5e487152))
+* **internal:** more robust bootstrap script ([f3e33d5](https://github.com/Benchify/formalize-python/commit/f3e33d5e7b7652b0764ae2f39ca186aaba690a4c))
+* **internal:** reformat pyproject.toml ([cc326cf](https://github.com/Benchify/formalize-python/commit/cc326cfca752a635b78a66cc326fae8b58f62ad4))
+* **internal:** update gitignore ([05eef18](https://github.com/Benchify/formalize-python/commit/05eef18b272c2be5047fa6c51504fc1e49aca1cb))
+
## 1.3.0 (2026-03-17)
Full Changelog: [v1.2.0...v1.3.0](https://github.com/Benchify/formalize-python/compare/v1.2.0...v1.3.0)
diff --git a/pyproject.toml b/pyproject.toml
index 0eb2b6c..0e7ba01 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "benchify"
-version = "1.3.0"
+version = "1.4.0"
description = "The official Python library for the formalize API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/formalize/_version.py b/src/formalize/_version.py
index 7bc1e96..a03222b 100644
--- a/src/formalize/_version.py
+++ b/src/formalize/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "formalize"
-__version__ = "1.3.0" # x-release-please-version
+__version__ = "1.4.0" # x-release-please-version