From 2ecd25a334d47128dc1728266bf3df0fcbdd8bc8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 12 Mar 2026 16:39:33 +0000
Subject: [PATCH 01/14] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 08fda43..627fe9f 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openregister%2Fopenregister-3e550ef1f7516fd49b02364a8cd188082775dd811d3a79ea35894277b39932c7.yml
openapi_spec_hash: 77b2dc3a4de8f91c7d1a31fdb60513ae
-config_hash: b9f5716a354cbb7263fcab72fb138615
+config_hash: 73cdb56f803707210e434d210d255083
From 061f4438184e642ae906036d6bbfbfd11e1e986e Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Thu, 12 Mar 2026 16:47:29 +0000
Subject: [PATCH 02/14] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 627fe9f..08fda43 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openregister%2Fopenregister-3e550ef1f7516fd49b02364a8cd188082775dd811d3a79ea35894277b39932c7.yml
openapi_spec_hash: 77b2dc3a4de8f91c7d1a31fdb60513ae
-config_hash: 73cdb56f803707210e434d210d255083
+config_hash: b9f5716a354cbb7263fcab72fb138615
From 1213d01413fd1cde4611f955ba443fb85fce6398 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sun, 15 Mar 2026 14:39:10 +0000
Subject: [PATCH 03/14] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 08fda43..8798bb6 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openregister%2Fopenregister-3e550ef1f7516fd49b02364a8cd188082775dd811d3a79ea35894277b39932c7.yml
openapi_spec_hash: 77b2dc3a4de8f91c7d1a31fdb60513ae
-config_hash: b9f5716a354cbb7263fcab72fb138615
+config_hash: 44b769f278e8816753471b99dcfe94b2
From 777d63dfff7da6a03f6ec8fe7110f0f8905258a4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Sun, 15 Mar 2026 14:44:29 +0000
Subject: [PATCH 04/14] codegen metadata
---
.stats.yml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/.stats.yml b/.stats.yml
index 8798bb6..cc88b72 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openregister%2Fopenregister-3e550ef1f7516fd49b02364a8cd188082775dd811d3a79ea35894277b39932c7.yml
openapi_spec_hash: 77b2dc3a4de8f91c7d1a31fdb60513ae
-config_hash: 44b769f278e8816753471b99dcfe94b2
+config_hash: a5cd2fc3ed8179ddfbed36dc9cb58f7a
From 3027dda1781ce25e428551cb4cbdd4f9b6efddc8 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 13:44:54 +0000
Subject: [PATCH 05/14] fix(pydantic): do not pass `by_alias` unless set
---
src/openregister/_compat.py | 11 +++++++++--
1 file changed, 9 insertions(+), 2 deletions(-)
diff --git a/src/openregister/_compat.py b/src/openregister/_compat.py
index 786ff42..e6690a4 100644
--- a/src/openregister/_compat.py
+++ b/src/openregister/_compat.py
@@ -2,7 +2,7 @@
from typing import TYPE_CHECKING, Any, Union, Generic, TypeVar, Callable, cast, overload
from datetime import date, datetime
-from typing_extensions import Self, Literal
+from typing_extensions import Self, Literal, TypedDict
import pydantic
from pydantic.fields import FieldInfo
@@ -131,6 +131,10 @@ def model_json(model: pydantic.BaseModel, *, indent: int | None = None) -> str:
return model.model_dump_json(indent=indent)
+class _ModelDumpKwargs(TypedDict, total=False):
+ by_alias: bool
+
+
def model_dump(
model: pydantic.BaseModel,
*,
@@ -142,6 +146,9 @@ def model_dump(
by_alias: bool | None = None,
) -> dict[str, Any]:
if (not PYDANTIC_V1) or hasattr(model, "model_dump"):
+ kwargs: _ModelDumpKwargs = {}
+ if by_alias is not None:
+ kwargs["by_alias"] = by_alias
return model.model_dump(
mode=mode,
exclude=exclude,
@@ -149,7 +156,7 @@ def model_dump(
exclude_defaults=exclude_defaults,
# warnings are not supported in Pydantic v1
warnings=True if PYDANTIC_V1 else warnings,
- by_alias=by_alias,
+ **kwargs,
)
return cast(
"dict[str, Any]",
From 2f29556e998619b9a3118e8ed73a648a93a12860 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 13:50:13 +0000
Subject: [PATCH 06/14] fix(deps): bump minimum typing-extensions version
---
pyproject.toml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/pyproject.toml b/pyproject.toml
index 3687112..e2df70f 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -11,7 +11,7 @@ authors = [
dependencies = [
"httpx>=0.23.0, <1",
"pydantic>=1.9.0, <3",
- "typing-extensions>=4.10, <5",
+ "typing-extensions>=4.14, <5",
"anyio>=3.5.0, <5",
"distro>=1.7.0, <2",
"sniffio",
From 4b0ff8267fd4ba3b0c6990d87fdd87b7fc1c3fa4 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 17 Mar 2026 13:53:45 +0000
Subject: [PATCH 07/14] chore(internal): tweak CI branches
---
.github/workflows/ci.yml | 14 ++++++++------
1 file changed, 8 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index b8065fb..52b3444 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,12 +1,14 @@
name: CI
on:
push:
- branches-ignore:
- - 'generated'
- - 'codegen/**'
- - 'integrated/**'
- - 'stl-preview-head/**'
- - 'stl-preview-base/**'
+ branches:
+ - '**'
+ - '!integrated/**'
+ - '!stl-preview-head/**'
+ - '!stl-preview-base/**'
+ - '!generated'
+ - '!codegen/**'
+ - 'codegen/stl/**'
pull_request:
branches-ignore:
- 'stl-preview-head/**'
From 924be9b09c0ee804aa934b3615db0c6089d2e224 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:54:21 +0000
Subject: [PATCH 08/14] fix: sanitize endpoint path params
---
src/openregister/_utils/__init__.py | 1 +
src/openregister/_utils/_path.py | 127 +++++++++++++++++++++++++
src/openregister/resources/company.py | 30 +++---
src/openregister/resources/document.py | 6 +-
src/openregister/resources/person.py | 9 +-
tests/test_utils/test_path.py | 89 +++++++++++++++++
6 files changed, 240 insertions(+), 22 deletions(-)
create mode 100644 src/openregister/_utils/_path.py
create mode 100644 tests/test_utils/test_path.py
diff --git a/src/openregister/_utils/__init__.py b/src/openregister/_utils/__init__.py
index dc64e29..10cb66d 100644
--- a/src/openregister/_utils/__init__.py
+++ b/src/openregister/_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/openregister/_utils/_path.py b/src/openregister/_utils/_path.py
new file mode 100644
index 0000000..4d6e1e4
--- /dev/null
+++ b/src/openregister/_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/openregister/resources/company.py b/src/openregister/resources/company.py
index 55cac45..2292e2e 100644
--- a/src/openregister/resources/company.py
+++ b/src/openregister/resources/company.py
@@ -6,7 +6,7 @@
from ..types import company_get_owners_v1_params, company_get_details_v1_params
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 (
@@ -73,7 +73,7 @@ def get_contact_v0(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return self._get(
- f"/v0/company/{company_id}/contact",
+ path_template("/v0/company/{company_id}/contact", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -115,7 +115,7 @@ def get_details_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return self._get(
- f"/v1/company/{company_id}",
+ path_template("/v1/company/{company_id}", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -158,7 +158,7 @@ def get_financials_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return self._get(
- f"/v1/company/{company_id}/financials",
+ path_template("/v1/company/{company_id}/financials", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -191,7 +191,7 @@ def get_historical_owners_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return self._get(
- f"/v1/company/{company_id}/owners/historical",
+ path_template("/v1/company/{company_id}/owners/historical", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -224,7 +224,7 @@ def get_holdings_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return self._get(
- f"/v1/company/{company_id}/holdings",
+ path_template("/v1/company/{company_id}/holdings", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -267,7 +267,7 @@ def get_owners_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return self._get(
- f"/v1/company/{company_id}/owners",
+ path_template("/v1/company/{company_id}/owners", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -310,7 +310,7 @@ def get_ubos_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return self._get(
- f"/v1/company/{company_id}/ubo",
+ path_template("/v1/company/{company_id}/ubo", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -364,7 +364,7 @@ async def get_contact_v0(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return await self._get(
- f"/v0/company/{company_id}/contact",
+ path_template("/v0/company/{company_id}/contact", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -406,7 +406,7 @@ async def get_details_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return await self._get(
- f"/v1/company/{company_id}",
+ path_template("/v1/company/{company_id}", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -449,7 +449,7 @@ async def get_financials_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return await self._get(
- f"/v1/company/{company_id}/financials",
+ path_template("/v1/company/{company_id}/financials", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -482,7 +482,7 @@ async def get_historical_owners_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return await self._get(
- f"/v1/company/{company_id}/owners/historical",
+ path_template("/v1/company/{company_id}/owners/historical", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -515,7 +515,7 @@ async def get_holdings_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return await self._get(
- f"/v1/company/{company_id}/holdings",
+ path_template("/v1/company/{company_id}/holdings", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -558,7 +558,7 @@ async def get_owners_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return await self._get(
- f"/v1/company/{company_id}/owners",
+ path_template("/v1/company/{company_id}/owners", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers,
extra_query=extra_query,
@@ -601,7 +601,7 @@ async def get_ubos_v1(
if not company_id:
raise ValueError(f"Expected a non-empty value for `company_id` but received {company_id!r}")
return await self._get(
- f"/v1/company/{company_id}/ubo",
+ path_template("/v1/company/{company_id}/ubo", company_id=company_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/openregister/resources/document.py b/src/openregister/resources/document.py
index 4e203d4..19e4c36 100644
--- a/src/openregister/resources/document.py
+++ b/src/openregister/resources/document.py
@@ -8,7 +8,7 @@
from ..types import document_get_realtime_v1_params
from .._types import Body, Query, Headers, NotGiven, 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 (
@@ -70,7 +70,7 @@ def get_cached_v1(
if not document_id:
raise ValueError(f"Expected a non-empty value for `document_id` but received {document_id!r}")
return self._get(
- f"/v1/document/{document_id}",
+ path_template("/v1/document/{document_id}", document_id=document_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -173,7 +173,7 @@ async def get_cached_v1(
if not document_id:
raise ValueError(f"Expected a non-empty value for `document_id` but received {document_id!r}")
return await self._get(
- f"/v1/document/{document_id}",
+ path_template("/v1/document/{document_id}", document_id=document_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/src/openregister/resources/person.py b/src/openregister/resources/person.py
index 0f315e6..8a2fb41 100644
--- a/src/openregister/resources/person.py
+++ b/src/openregister/resources/person.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 get_details_v1(
if not person_id:
raise ValueError(f"Expected a non-empty value for `person_id` but received {person_id!r}")
return self._get(
- f"/v1/person/{person_id}",
+ path_template("/v1/person/{person_id}", person_id=person_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -99,7 +100,7 @@ def get_holdings_v1(
if not person_id:
raise ValueError(f"Expected a non-empty value for `person_id` but received {person_id!r}")
return self._get(
- f"/v1/person/{person_id}/holdings",
+ path_template("/v1/person/{person_id}/holdings", person_id=person_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -153,7 +154,7 @@ async def get_details_v1(
if not person_id:
raise ValueError(f"Expected a non-empty value for `person_id` but received {person_id!r}")
return await self._get(
- f"/v1/person/{person_id}",
+ path_template("/v1/person/{person_id}", person_id=person_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
@@ -186,7 +187,7 @@ async def get_holdings_v1(
if not person_id:
raise ValueError(f"Expected a non-empty value for `person_id` but received {person_id!r}")
return await self._get(
- f"/v1/person/{person_id}/holdings",
+ path_template("/v1/person/{person_id}/holdings", person_id=person_id),
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
),
diff --git a/tests/test_utils/test_path.py b/tests/test_utils/test_path.py
new file mode 100644
index 0000000..c9a29e9
--- /dev/null
+++ b/tests/test_utils/test_path.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+from typing import Any
+
+import pytest
+
+from openregister._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 26e53e088b1e334b1adfc6baa54a997831f9ba84 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Tue, 24 Mar 2026 05:12:43 +0000
Subject: [PATCH 09/14] 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 f053a64334e4de4eea8ed8d7977b679b8c8fe019 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Wed, 25 Mar 2026 03:30:41 +0000
Subject: [PATCH 10/14] 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 52b3444..ef802a3 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/openregister-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
@@ -38,7 +38,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 87468c0ba7e8be29d701bf25fa6c1d3606d85446 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 06:47:36 +0000
Subject: [PATCH 11/14] feat(internal): implement indices array format for
query and form serialization
---
src/openregister/_qs.py | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/openregister/_qs.py b/src/openregister/_qs.py
index ada6fd3..de8c99b 100644
--- a/src/openregister/_qs.py
+++ b/src/openregister/_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 183ed3ceed7e4cee6ac1d4792672e55480de82db Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 12:40:08 +0000
Subject: [PATCH 12/14] codegen metadata
---
.stats.yml | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/.stats.yml b/.stats.yml
index cc88b72..fb1379c 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
configured_endpoints: 15
-openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openregister%2Fopenregister-3e550ef1f7516fd49b02364a8cd188082775dd811d3a79ea35894277b39932c7.yml
-openapi_spec_hash: 77b2dc3a4de8f91c7d1a31fdb60513ae
+openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openregister%2Fopenregister-cdea05542fb87dc7f00544bdd53fd82469ad985a48aa0d96fdd74011464e3a5d.yml
+openapi_spec_hash: e49334a0e9b0d800974f7453910ab5ee
config_hash: a5cd2fc3ed8179ddfbed36dc9cb58f7a
From 205151139e0f27c8d731890a0341276bedcddb8d Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 12:41:47 +0000
Subject: [PATCH 13/14] feat(api): monitoring endpoints
---
.stats.yml | 4 +-
api.md | 14 +
src/openregister/_client.py | 39 +-
src/openregister/resources/__init__.py | 14 +
src/openregister/resources/monitor.py | 341 ++++++++++++++++++
src/openregister/types/__init__.py | 3 +
.../types/monitor_create_params.py | 32 ++
.../types/monitor_create_response.py | 31 ++
.../types/monitor_list_response.py | 36 ++
tests/api_resources/test_monitor.py | 244 +++++++++++++
10 files changed, 755 insertions(+), 3 deletions(-)
create mode 100644 src/openregister/resources/monitor.py
create mode 100644 src/openregister/types/monitor_create_params.py
create mode 100644 src/openregister/types/monitor_create_response.py
create mode 100644 src/openregister/types/monitor_list_response.py
create mode 100644 tests/api_resources/test_monitor.py
diff --git a/.stats.yml b/.stats.yml
index fb1379c..ded0391 100644
--- a/.stats.yml
+++ b/.stats.yml
@@ -1,4 +1,4 @@
-configured_endpoints: 15
+configured_endpoints: 18
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openregister%2Fopenregister-cdea05542fb87dc7f00544bdd53fd82469ad985a48aa0d96fdd74011464e3a5d.yml
openapi_spec_hash: e49334a0e9b0d800974f7453910ab5ee
-config_hash: a5cd2fc3ed8179ddfbed36dc9cb58f7a
+config_hash: 6b8c0142ef1c8e22e9ca6b5b834e2a04
diff --git a/api.md b/api.md
index 0a52e2c..aad2230 100644
--- a/api.md
+++ b/api.md
@@ -91,3 +91,17 @@ Methods:
- client.person.get_details_v1(person_id) -> PersonGetDetailsV1Response
- client.person.get_holdings_v1(person_id) -> PersonGetHoldingsV1Response
+
+# Monitor
+
+Types:
+
+```python
+from openregister.types import MonitorCreateResponse, MonitorListResponse
+```
+
+Methods:
+
+- client.monitor.create(\*\*params) -> MonitorCreateResponse
+- client.monitor.list() -> MonitorListResponse
+- client.monitor.delete(entity_id) -> None
diff --git a/src/openregister/_client.py b/src/openregister/_client.py
index e215734..42b242a 100644
--- a/src/openregister/_client.py
+++ b/src/openregister/_client.py
@@ -31,10 +31,11 @@
)
if TYPE_CHECKING:
- from .resources import person, search, company, document
+ from .resources import person, search, company, monitor, document
from .resources.person import PersonResource, AsyncPersonResource
from .resources.search import SearchResource, AsyncSearchResource
from .resources.company import CompanyResource, AsyncCompanyResource
+ from .resources.monitor import MonitorResource, AsyncMonitorResource
from .resources.document import DocumentResource, AsyncDocumentResource
__all__ = [
@@ -128,6 +129,12 @@ def person(self) -> PersonResource:
return PersonResource(self)
+ @cached_property
+ def monitor(self) -> MonitorResource:
+ from .resources.monitor import MonitorResource
+
+ return MonitorResource(self)
+
@cached_property
def with_raw_response(self) -> OpenregisterWithRawResponse:
return OpenregisterWithRawResponse(self)
@@ -320,6 +327,12 @@ def person(self) -> AsyncPersonResource:
return AsyncPersonResource(self)
+ @cached_property
+ def monitor(self) -> AsyncMonitorResource:
+ from .resources.monitor import AsyncMonitorResource
+
+ return AsyncMonitorResource(self)
+
@cached_property
def with_raw_response(self) -> AsyncOpenregisterWithRawResponse:
return AsyncOpenregisterWithRawResponse(self)
@@ -463,6 +476,12 @@ def person(self) -> person.PersonResourceWithRawResponse:
return PersonResourceWithRawResponse(self._client.person)
+ @cached_property
+ def monitor(self) -> monitor.MonitorResourceWithRawResponse:
+ from .resources.monitor import MonitorResourceWithRawResponse
+
+ return MonitorResourceWithRawResponse(self._client.monitor)
+
class AsyncOpenregisterWithRawResponse:
_client: AsyncOpenregister
@@ -494,6 +513,12 @@ def person(self) -> person.AsyncPersonResourceWithRawResponse:
return AsyncPersonResourceWithRawResponse(self._client.person)
+ @cached_property
+ def monitor(self) -> monitor.AsyncMonitorResourceWithRawResponse:
+ from .resources.monitor import AsyncMonitorResourceWithRawResponse
+
+ return AsyncMonitorResourceWithRawResponse(self._client.monitor)
+
class OpenregisterWithStreamedResponse:
_client: Openregister
@@ -525,6 +550,12 @@ def person(self) -> person.PersonResourceWithStreamingResponse:
return PersonResourceWithStreamingResponse(self._client.person)
+ @cached_property
+ def monitor(self) -> monitor.MonitorResourceWithStreamingResponse:
+ from .resources.monitor import MonitorResourceWithStreamingResponse
+
+ return MonitorResourceWithStreamingResponse(self._client.monitor)
+
class AsyncOpenregisterWithStreamedResponse:
_client: AsyncOpenregister
@@ -556,6 +587,12 @@ def person(self) -> person.AsyncPersonResourceWithStreamingResponse:
return AsyncPersonResourceWithStreamingResponse(self._client.person)
+ @cached_property
+ def monitor(self) -> monitor.AsyncMonitorResourceWithStreamingResponse:
+ from .resources.monitor import AsyncMonitorResourceWithStreamingResponse
+
+ return AsyncMonitorResourceWithStreamingResponse(self._client.monitor)
+
Client = Openregister
diff --git a/src/openregister/resources/__init__.py b/src/openregister/resources/__init__.py
index f5b0c5c..273d61e 100644
--- a/src/openregister/resources/__init__.py
+++ b/src/openregister/resources/__init__.py
@@ -24,6 +24,14 @@
CompanyResourceWithStreamingResponse,
AsyncCompanyResourceWithStreamingResponse,
)
+from .monitor import (
+ MonitorResource,
+ AsyncMonitorResource,
+ MonitorResourceWithRawResponse,
+ AsyncMonitorResourceWithRawResponse,
+ MonitorResourceWithStreamingResponse,
+ AsyncMonitorResourceWithStreamingResponse,
+)
from .document import (
DocumentResource,
AsyncDocumentResource,
@@ -58,4 +66,10 @@
"AsyncPersonResourceWithRawResponse",
"PersonResourceWithStreamingResponse",
"AsyncPersonResourceWithStreamingResponse",
+ "MonitorResource",
+ "AsyncMonitorResource",
+ "MonitorResourceWithRawResponse",
+ "AsyncMonitorResourceWithRawResponse",
+ "MonitorResourceWithStreamingResponse",
+ "AsyncMonitorResourceWithStreamingResponse",
]
diff --git a/src/openregister/resources/monitor.py b/src/openregister/resources/monitor.py
new file mode 100644
index 0000000..9e4b657
--- /dev/null
+++ b/src/openregister/resources/monitor.py
@@ -0,0 +1,341 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal
+
+import httpx
+
+from ..types import monitor_create_params
+from .._types import Body, Query, Headers, NoneType, NotGiven, 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 .._base_client import make_request_options
+from ..types.monitor_list_response import MonitorListResponse
+from ..types.monitor_create_response import MonitorCreateResponse
+
+__all__ = ["MonitorResource", "AsyncMonitorResource"]
+
+
+class MonitorResource(SyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> MonitorResourceWithRawResponse:
+ """
+ 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/oregister/openregister-python#accessing-raw-response-data-eg-headers
+ """
+ return MonitorResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> MonitorResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/oregister/openregister-python#with_streaming_response
+ """
+ return MonitorResourceWithStreamingResponse(self)
+
+ def create(
+ self,
+ *,
+ entity_id: str,
+ entity_type: Literal["company", "person"],
+ preferences: List[
+ Literal[
+ "basic", "representation", "financials", "documents", "ownership", "holdings", "management_positions"
+ ]
+ ],
+ # 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,
+ ) -> MonitorCreateResponse:
+ """
+ Create webhook monitor item
+
+ Args:
+ entity_id: For `company` this is the register ID (e.g. `DE-HRB-F1103-267645`). For `person`
+ this is the person UUID.
+
+ entity_type: Type of the entity to monitor.
+
+ preferences: Preferences for the entity to monitor. Use `WebhookMonitorCompanyPreference`
+ values when `entity_type` is `company`, and `WebhookMonitorPersonPreference`
+ values when `entity_type` is `person`.
+
+ 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
+ """
+ return self._post(
+ "/v1/monitor",
+ body=maybe_transform(
+ {
+ "entity_id": entity_id,
+ "entity_type": entity_type,
+ "preferences": preferences,
+ },
+ monitor_create_params.MonitorCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MonitorCreateResponse,
+ )
+
+ def list(
+ self,
+ *,
+ # 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,
+ ) -> MonitorListResponse:
+ """List webhook monitor items"""
+ return self._get(
+ "/v1/monitor",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MonitorListResponse,
+ )
+
+ def delete(
+ self,
+ entity_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,
+ ) -> None:
+ """
+ Delete webhook monitor item
+
+ 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 entity_id:
+ raise ValueError(f"Expected a non-empty value for `entity_id` but received {entity_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return self._delete(
+ path_template("/v1/monitor/{entity_id}", entity_id=entity_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class AsyncMonitorResource(AsyncAPIResource):
+ @cached_property
+ def with_raw_response(self) -> AsyncMonitorResourceWithRawResponse:
+ """
+ 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/oregister/openregister-python#accessing-raw-response-data-eg-headers
+ """
+ return AsyncMonitorResourceWithRawResponse(self)
+
+ @cached_property
+ def with_streaming_response(self) -> AsyncMonitorResourceWithStreamingResponse:
+ """
+ An alternative to `.with_raw_response` that doesn't eagerly read the response body.
+
+ For more information, see https://www.github.com/oregister/openregister-python#with_streaming_response
+ """
+ return AsyncMonitorResourceWithStreamingResponse(self)
+
+ async def create(
+ self,
+ *,
+ entity_id: str,
+ entity_type: Literal["company", "person"],
+ preferences: List[
+ Literal[
+ "basic", "representation", "financials", "documents", "ownership", "holdings", "management_positions"
+ ]
+ ],
+ # 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,
+ ) -> MonitorCreateResponse:
+ """
+ Create webhook monitor item
+
+ Args:
+ entity_id: For `company` this is the register ID (e.g. `DE-HRB-F1103-267645`). For `person`
+ this is the person UUID.
+
+ entity_type: Type of the entity to monitor.
+
+ preferences: Preferences for the entity to monitor. Use `WebhookMonitorCompanyPreference`
+ values when `entity_type` is `company`, and `WebhookMonitorPersonPreference`
+ values when `entity_type` is `person`.
+
+ 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
+ """
+ return await self._post(
+ "/v1/monitor",
+ body=await async_maybe_transform(
+ {
+ "entity_id": entity_id,
+ "entity_type": entity_type,
+ "preferences": preferences,
+ },
+ monitor_create_params.MonitorCreateParams,
+ ),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MonitorCreateResponse,
+ )
+
+ async def list(
+ self,
+ *,
+ # 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,
+ ) -> MonitorListResponse:
+ """List webhook monitor items"""
+ return await self._get(
+ "/v1/monitor",
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=MonitorListResponse,
+ )
+
+ async def delete(
+ self,
+ entity_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,
+ ) -> None:
+ """
+ Delete webhook monitor item
+
+ 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 entity_id:
+ raise ValueError(f"Expected a non-empty value for `entity_id` but received {entity_id!r}")
+ extra_headers = {"Accept": "*/*", **(extra_headers or {})}
+ return await self._delete(
+ path_template("/v1/monitor/{entity_id}", entity_id=entity_id),
+ options=make_request_options(
+ extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
+ ),
+ cast_to=NoneType,
+ )
+
+
+class MonitorResourceWithRawResponse:
+ def __init__(self, monitor: MonitorResource) -> None:
+ self._monitor = monitor
+
+ self.create = to_raw_response_wrapper(
+ monitor.create,
+ )
+ self.list = to_raw_response_wrapper(
+ monitor.list,
+ )
+ self.delete = to_raw_response_wrapper(
+ monitor.delete,
+ )
+
+
+class AsyncMonitorResourceWithRawResponse:
+ def __init__(self, monitor: AsyncMonitorResource) -> None:
+ self._monitor = monitor
+
+ self.create = async_to_raw_response_wrapper(
+ monitor.create,
+ )
+ self.list = async_to_raw_response_wrapper(
+ monitor.list,
+ )
+ self.delete = async_to_raw_response_wrapper(
+ monitor.delete,
+ )
+
+
+class MonitorResourceWithStreamingResponse:
+ def __init__(self, monitor: MonitorResource) -> None:
+ self._monitor = monitor
+
+ self.create = to_streamed_response_wrapper(
+ monitor.create,
+ )
+ self.list = to_streamed_response_wrapper(
+ monitor.list,
+ )
+ self.delete = to_streamed_response_wrapper(
+ monitor.delete,
+ )
+
+
+class AsyncMonitorResourceWithStreamingResponse:
+ def __init__(self, monitor: AsyncMonitorResource) -> None:
+ self._monitor = monitor
+
+ self.create = async_to_streamed_response_wrapper(
+ monitor.create,
+ )
+ self.list = async_to_streamed_response_wrapper(
+ monitor.list,
+ )
+ self.delete = async_to_streamed_response_wrapper(
+ monitor.delete,
+ )
diff --git a/src/openregister/types/__init__.py b/src/openregister/types/__init__.py
index ba61900..0327aec 100644
--- a/src/openregister/types/__init__.py
+++ b/src/openregister/types/__init__.py
@@ -23,6 +23,9 @@
from .representation_role import RepresentationRole as RepresentationRole
from .company_register_type import CompanyRegisterType as CompanyRegisterType
from .company_relation_type import CompanyRelationType as CompanyRelationType
+from .monitor_create_params import MonitorCreateParams as MonitorCreateParams
+from .monitor_list_response import MonitorListResponse as MonitorListResponse
+from .monitor_create_response import MonitorCreateResponse as MonitorCreateResponse
from .search_filter_base_param import SearchFilterBaseParam as SearchFilterBaseParam
from .company_owner_legal_person import CompanyOwnerLegalPerson as CompanyOwnerLegalPerson
from .company_get_owners_v1_params import CompanyGetOwnersV1Params as CompanyGetOwnersV1Params
diff --git a/src/openregister/types/monitor_create_params.py b/src/openregister/types/monitor_create_params.py
new file mode 100644
index 0000000..908fc21
--- /dev/null
+++ b/src/openregister/types/monitor_create_params.py
@@ -0,0 +1,32 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from __future__ import annotations
+
+from typing import List
+from typing_extensions import Literal, Required, TypedDict
+
+__all__ = ["MonitorCreateParams"]
+
+
+class MonitorCreateParams(TypedDict, total=False):
+ entity_id: Required[str]
+ """For `company` this is the register ID (e.g.
+
+ `DE-HRB-F1103-267645`). For `person` this is the person UUID.
+ """
+
+ entity_type: Required[Literal["company", "person"]]
+ """Type of the entity to monitor."""
+
+ preferences: Required[
+ List[
+ Literal[
+ "basic", "representation", "financials", "documents", "ownership", "holdings", "management_positions"
+ ]
+ ]
+ ]
+ """
+ Preferences for the entity to monitor. Use `WebhookMonitorCompanyPreference`
+ values when `entity_type` is `company`, and `WebhookMonitorPersonPreference`
+ values when `entity_type` is `person`.
+ """
diff --git a/src/openregister/types/monitor_create_response.py b/src/openregister/types/monitor_create_response.py
new file mode 100644
index 0000000..8ff73d2
--- /dev/null
+++ b/src/openregister/types/monitor_create_response.py
@@ -0,0 +1,31 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["MonitorCreateResponse"]
+
+
+class MonitorCreateResponse(BaseModel):
+ disabled: bool
+ """Whether the monitor item is disabled."""
+
+ entity_id: str
+ """For `company` this is the register ID (e.g.
+
+ `DE-HRB-F1103-267645`). For `person` this is the person UUID.
+ """
+
+ entity_type: Literal["company", "person"]
+ """Type of the entity to monitor."""
+
+ preferences: List[
+ Literal["basic", "representation", "financials", "documents", "ownership", "holdings", "management_positions"]
+ ]
+ """
+ Preferences for the entity to monitor. Use `WebhookMonitorCompanyPreference`
+ values when `entity_type` is `company`, and `WebhookMonitorPersonPreference`
+ values when `entity_type` is `person`.
+ """
diff --git a/src/openregister/types/monitor_list_response.py b/src/openregister/types/monitor_list_response.py
new file mode 100644
index 0000000..265a983
--- /dev/null
+++ b/src/openregister/types/monitor_list_response.py
@@ -0,0 +1,36 @@
+# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
+
+from typing import List
+from typing_extensions import Literal
+
+from .._models import BaseModel
+
+__all__ = ["MonitorListResponse", "Item"]
+
+
+class Item(BaseModel):
+ disabled: bool
+ """Whether the monitor item is disabled."""
+
+ entity_id: str
+ """For `company` this is the register ID (e.g.
+
+ `DE-HRB-F1103-267645`). For `person` this is the person UUID.
+ """
+
+ entity_type: Literal["company", "person"]
+ """Type of the entity to monitor."""
+
+ preferences: List[
+ Literal["basic", "representation", "financials", "documents", "ownership", "holdings", "management_positions"]
+ ]
+ """
+ Preferences for the entity to monitor. Use `WebhookMonitorCompanyPreference`
+ values when `entity_type` is `company`, and `WebhookMonitorPersonPreference`
+ values when `entity_type` is `person`.
+ """
+
+
+class MonitorListResponse(BaseModel):
+ items: List[Item]
+ """List of webhook monitor items."""
diff --git a/tests/api_resources/test_monitor.py b/tests/api_resources/test_monitor.py
new file mode 100644
index 0000000..8a4720e
--- /dev/null
+++ b/tests/api_resources/test_monitor.py
@@ -0,0 +1,244 @@
+# 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 tests.utils import assert_matches_type
+from openregister import Openregister, AsyncOpenregister
+from openregister.types import MonitorListResponse, MonitorCreateResponse
+
+base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
+
+
+class TestMonitor:
+ 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_create(self, client: Openregister) -> None:
+ monitor = client.monitor.create(
+ entity_id="entity_id",
+ entity_type="company",
+ preferences=["basic"],
+ )
+ assert_matches_type(MonitorCreateResponse, monitor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_create(self, client: Openregister) -> None:
+ response = client.monitor.with_raw_response.create(
+ entity_id="entity_id",
+ entity_type="company",
+ preferences=["basic"],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ monitor = response.parse()
+ assert_matches_type(MonitorCreateResponse, monitor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_create(self, client: Openregister) -> None:
+ with client.monitor.with_streaming_response.create(
+ entity_id="entity_id",
+ entity_type="company",
+ preferences=["basic"],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ monitor = response.parse()
+ assert_matches_type(MonitorCreateResponse, monitor, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_list(self, client: Openregister) -> None:
+ monitor = client.monitor.list()
+ assert_matches_type(MonitorListResponse, monitor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_list(self, client: Openregister) -> None:
+ response = client.monitor.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ monitor = response.parse()
+ assert_matches_type(MonitorListResponse, monitor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_list(self, client: Openregister) -> None:
+ with client.monitor.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ monitor = response.parse()
+ assert_matches_type(MonitorListResponse, monitor, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_method_delete(self, client: Openregister) -> None:
+ monitor = client.monitor.delete(
+ "entity_id",
+ )
+ assert monitor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_raw_response_delete(self, client: Openregister) -> None:
+ response = client.monitor.with_raw_response.delete(
+ "entity_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ monitor = response.parse()
+ assert monitor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_streaming_response_delete(self, client: Openregister) -> None:
+ with client.monitor.with_streaming_response.delete(
+ "entity_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ monitor = response.parse()
+ assert monitor is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ def test_path_params_delete(self, client: Openregister) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `entity_id` but received ''"):
+ client.monitor.with_raw_response.delete(
+ "",
+ )
+
+
+class TestAsyncMonitor:
+ 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_create(self, async_client: AsyncOpenregister) -> None:
+ monitor = await async_client.monitor.create(
+ entity_id="entity_id",
+ entity_type="company",
+ preferences=["basic"],
+ )
+ assert_matches_type(MonitorCreateResponse, monitor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_create(self, async_client: AsyncOpenregister) -> None:
+ response = await async_client.monitor.with_raw_response.create(
+ entity_id="entity_id",
+ entity_type="company",
+ preferences=["basic"],
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ monitor = await response.parse()
+ assert_matches_type(MonitorCreateResponse, monitor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_create(self, async_client: AsyncOpenregister) -> None:
+ async with async_client.monitor.with_streaming_response.create(
+ entity_id="entity_id",
+ entity_type="company",
+ preferences=["basic"],
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ monitor = await response.parse()
+ assert_matches_type(MonitorCreateResponse, monitor, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_list(self, async_client: AsyncOpenregister) -> None:
+ monitor = await async_client.monitor.list()
+ assert_matches_type(MonitorListResponse, monitor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_list(self, async_client: AsyncOpenregister) -> None:
+ response = await async_client.monitor.with_raw_response.list()
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ monitor = await response.parse()
+ assert_matches_type(MonitorListResponse, monitor, path=["response"])
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_list(self, async_client: AsyncOpenregister) -> None:
+ async with async_client.monitor.with_streaming_response.list() as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ monitor = await response.parse()
+ assert_matches_type(MonitorListResponse, monitor, path=["response"])
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_method_delete(self, async_client: AsyncOpenregister) -> None:
+ monitor = await async_client.monitor.delete(
+ "entity_id",
+ )
+ assert monitor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_raw_response_delete(self, async_client: AsyncOpenregister) -> None:
+ response = await async_client.monitor.with_raw_response.delete(
+ "entity_id",
+ )
+
+ assert response.is_closed is True
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+ monitor = await response.parse()
+ assert monitor is None
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_streaming_response_delete(self, async_client: AsyncOpenregister) -> None:
+ async with async_client.monitor.with_streaming_response.delete(
+ "entity_id",
+ ) as response:
+ assert not response.is_closed
+ assert response.http_request.headers.get("X-Stainless-Lang") == "python"
+
+ monitor = await response.parse()
+ assert monitor is None
+
+ assert cast(Any, response.is_closed) is True
+
+ @pytest.mark.skip(reason="Mock server tests are disabled")
+ @parametrize
+ async def test_path_params_delete(self, async_client: AsyncOpenregister) -> None:
+ with pytest.raises(ValueError, match=r"Expected a non-empty value for `entity_id` but received ''"):
+ await async_client.monitor.with_raw_response.delete(
+ "",
+ )
From 09fc565886cccd0b4e347311e88c2b9d0bb984c7 Mon Sep 17 00:00:00 2001
From: "stainless-app[bot]"
<142633134+stainless-app[bot]@users.noreply.github.com>
Date: Fri, 27 Mar 2026 12:42:01 +0000
Subject: [PATCH 14/14] release: 3.1.0
---
.release-please-manifest.json | 2 +-
CHANGELOG.md | 23 +++++++++++++++++++++++
pyproject.toml | 2 +-
src/openregister/_version.py | 2 +-
4 files changed, 26 insertions(+), 3 deletions(-)
diff --git a/.release-please-manifest.json b/.release-please-manifest.json
index 9202efd..e0dc500 100644
--- a/.release-please-manifest.json
+++ b/.release-please-manifest.json
@@ -1,3 +1,3 @@
{
- ".": "3.0.3"
+ ".": "3.1.0"
}
\ No newline at end of file
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4956924..872e43e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,28 @@
# Changelog
+## 3.1.0 (2026-03-27)
+
+Full Changelog: [v3.0.3...v3.1.0](https://github.com/oregister/openregister-python/compare/v3.0.3...v3.1.0)
+
+### Features
+
+* **api:** monitoring endpoints ([2051511](https://github.com/oregister/openregister-python/commit/205151139e0f27c8d731890a0341276bedcddb8d))
+* **internal:** implement indices array format for query and form serialization ([87468c0](https://github.com/oregister/openregister-python/commit/87468c0ba7e8be29d701bf25fa6c1d3606d85446))
+
+
+### Bug Fixes
+
+* **deps:** bump minimum typing-extensions version ([2f29556](https://github.com/oregister/openregister-python/commit/2f29556e998619b9a3118e8ed73a648a93a12860))
+* **pydantic:** do not pass `by_alias` unless set ([3027dda](https://github.com/oregister/openregister-python/commit/3027dda1781ce25e428551cb4cbdd4f9b6efddc8))
+* sanitize endpoint path params ([924be9b](https://github.com/oregister/openregister-python/commit/924be9b09c0ee804aa934b3615db0c6089d2e224))
+
+
+### Chores
+
+* **ci:** skip lint on metadata-only changes ([f053a64](https://github.com/oregister/openregister-python/commit/f053a64334e4de4eea8ed8d7977b679b8c8fe019))
+* **internal:** tweak CI branches ([4b0ff82](https://github.com/oregister/openregister-python/commit/4b0ff8267fd4ba3b0c6990d87fdd87b7fc1c3fa4))
+* **internal:** update gitignore ([26e53e0](https://github.com/oregister/openregister-python/commit/26e53e088b1e334b1adfc6baa54a997831f9ba84))
+
## 3.0.3 (2026-03-12)
Full Changelog: [v3.0.2...v3.0.3](https://github.com/oregister/openregister-python/compare/v3.0.2...v3.0.3)
diff --git a/pyproject.toml b/pyproject.toml
index e2df70f..d3696b0 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,6 +1,6 @@
[project]
name = "openregister-sdk"
-version = "3.0.3"
+version = "3.1.0"
description = "The official Python library for the openregister API"
dynamic = ["readme"]
license = "Apache-2.0"
diff --git a/src/openregister/_version.py b/src/openregister/_version.py
index 34f36c6..b7798f9 100644
--- a/src/openregister/_version.py
+++ b/src/openregister/_version.py
@@ -1,4 +1,4 @@
# File generated from our OpenAPI spec by Stainless. See CONTRIBUTING.md for details.
__title__ = "openregister"
-__version__ = "3.0.3" # x-release-please-version
+__version__ = "3.1.0" # x-release-please-version