From 57a91a5a9b59d4b8f0dc6b95f243ab5d4eaad69c Mon Sep 17 00:00:00 2001
From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 15:53:49 +0000
Subject: [PATCH 1/4] [fern-generated] Update SDK
Generated by Fern
CLI Version: unknown
Generators:
- fernapi/fern-python-sdk: 5.30.0
---
.fern/metadata.json | 10 +-
poetry.lock | 8 +-
pyproject.toml | 7 +-
reference.md | 196 +++++-
src/phenoml/agent/chat/client.py | 2 +-
src/phenoml/agent/chat/raw_client.py | 4 +
src/phenoml/client.py | 30 +-
src/phenoml/core/client_wrapper.py | 4 +-
src/phenoml/core/http_client.py | 101 ++-
src/phenoml/core/http_response.py | 4 +
src/phenoml/core/jsonable_encoder.py | 13 +
src/phenoml/core/oauth_token_provider.py | 8 +-
src/phenoml/core/pydantic_utilities.py | 134 ++--
src/phenoml/implementation_guides/__init__.py | 23 +-
.../implementation_guides/errors/__init__.py | 11 +-
.../errors/conflict_error.py | 10 +
.../implementation_guides/client.py | 226 ++++++-
.../implementation_guides/raw_client.py | 354 +++++++++-
.../implementation_guides/types/__init__.py | 12 +-
.../types/fhir_implementation_guide.py | 38 ++
.../types/implementation_guide_summary.py | 12 +-
.../implementation_guide_version_detail.py | 29 +
src/phenoml/lang2fhir_batch/client.py | 20 +-
src/phenoml/lang2fhir_batch/raw_client.py | 42 +-
.../lang2fhir_batch/types/batch_error.py | 12 +-
.../lang2fhir_batch/types/batch_job.py | 4 +-
src/phenoml/profiles/profiles/raw_client.py | 22 +
src/phenoml/profiles/versions/raw_client.py | 22 +
tests/utils/test_http_client.py | 112 +++-
...plementationGuides_implementationGuides.py | 29 +
wiremock/wiremock-mappings.json | 620 ++++++++++++++++--
31 files changed, 1862 insertions(+), 257 deletions(-)
create mode 100644 src/phenoml/implementation_guides/errors/conflict_error.py
create mode 100644 src/phenoml/implementation_guides/types/fhir_implementation_guide.py
create mode 100644 src/phenoml/implementation_guides/types/implementation_guide_version_detail.py
diff --git a/.fern/metadata.json b/.fern/metadata.json
index 508188d..a13b8b6 100644
--- a/.fern/metadata.json
+++ b/.fern/metadata.json
@@ -1,17 +1,17 @@
{
- "cliVersion": "5.118.0",
+ "cliVersion": "5.121.0",
"generatorName": "fernapi/fern-python-sdk",
- "generatorVersion": "5.18.1",
+ "generatorVersion": "5.30.0",
"generatorConfig": {
"client_class_name": "PhenomlClient",
"wire_tests": {
"enabled": true
}
},
- "originGitCommit": "26224ef37e22adb507f28c21e8991d4a27822bde",
+ "originGitCommit": "e555f47326197aad510a783942045e7c53476fc5",
"originGitCommitIsDirty": true,
"invokedBy": "ci",
"requestedVersion": "AUTO",
"ciProvider": "unknown",
- "sdkVersion": "17.0.0"
-}
+ "sdkVersion": "0.0.0.dev0"
+}
\ No newline at end of file
diff --git a/poetry.lock b/poetry.lock
index 3656b36..9d4e0e6 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -688,15 +688,15 @@ zstd = ["zstandard (>=0.18.0)"]
[[package]]
name = "httpx-aiohttp"
-version = "0.1.8"
+version = "0.1.12"
description = "Aiohttp transport for HTTPX"
optional = true
python-versions = ">=3.8"
groups = ["main"]
markers = "extra == \"aiohttp\""
files = [
- {file = "httpx_aiohttp-0.1.8-py3-none-any.whl", hash = "sha256:b7bd958d1331f3759a38a0ba22ad29832cb63ca69498c17735228055bf78fa7e"},
- {file = "httpx_aiohttp-0.1.8.tar.gz", hash = "sha256:756c5e74cdb568c3248ba63fe82bfe8bbe64b928728720f7eaac64b3cf46f308"},
+ {file = "httpx_aiohttp-0.1.12-py3-none-any.whl", hash = "sha256:5b0eac39a7f360fa7867a60bcb46bb1024eada9c01cbfecdb54dc1edb3fb7141"},
+ {file = "httpx_aiohttp-0.1.12.tar.gz", hash = "sha256:81feec51fd82c0ecfa0e9aaf1b1a6c2591260d5e2bcbeb7eb0277a78e610df2c"},
]
[package.dependencies]
@@ -1710,4 +1710,4 @@ aiohttp = ["aiohttp", "httpx-aiohttp"]
[metadata]
lock-version = "2.1"
python-versions = "^3.10"
-content-hash = "65ef513ef69a32344fa2133df094babe6493482ea7586d0e19edbcb2f4bbe7c8"
+content-hash = "d6f20007544f9ff1b04d92f47b351e1eda2f66672b4c62350d35c919bc2ae9f3"
diff --git a/pyproject.toml b/pyproject.toml
index 17a6e91..1dc545d 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ dynamic = ["version"]
[tool.poetry]
name = "phenoml"
-version = "17.0.0"
+version = "0.0.0.dev0"
description = ""
readme = "README.md"
authors = []
@@ -31,9 +31,6 @@ classifiers = [
packages = [
{ include = "phenoml", from = "src"}
]
-include = [
- { path = "src/phenoml/openapi/openapi.json", format = ["sdist", "wheel"] }
-]
[tool.poetry.urls]
Repository = 'https://github.com/phenoml/phenoml-python-sdk'
@@ -42,7 +39,7 @@ Repository = 'https://github.com/phenoml/phenoml-python-sdk'
python = "^3.10"
aiohttp = { version = ">=3.14.1,<4", optional = true, python = ">=3.10"}
httpx = ">=0.21.2"
-httpx-aiohttp = { version = "0.1.8", optional = true, python = ">=3.10"}
+httpx-aiohttp = { version = "^0.1.8", optional = true, python = ">=3.10"}
pydantic = ">= 1.9.2"
pydantic-core = ">=2.18.2,<3.0.0"
typing_extensions = ">= 4.0.0"
diff --git a/reference.md b/reference.md
index 99385a6..63620b5 100644
--- a/reference.md
+++ b/reference.md
@@ -4689,11 +4689,9 @@ client.implementation_guides.implementation_guides.update(
-
-Deletes the stored metadata for an implementation guide — its
-profile_context and timestamps. Member profiles keep their
-implementation_guide assignment, so a guide still referenced by at least
-one profile continues to appear in listings, just without context or
-timestamps.
+Deletes the stored name-level metadata and any exact canonical package
+versions beneath the guide. Legacy member profile assignments are not
+changed.
@@ -4751,6 +4749,184 @@ client.implementation_guides.implementation_guides.delete(
+
+
+
+
+client.implementation_guides.implementation_guides.create_version(...) -> ImplementationGuideVersionDetail
+
+-
+
+#### 📝 Description
+
+
+-
+
+
+-
+
+Publishes an exact package beneath this guide family. PR 2 temporarily
+permits one exact package version per guide family; publishing another
+version returns `409 Conflict` until multi-version package support lands.
+
+
+
+
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from phenoml import PhenomlClient
+from phenoml.environment import PhenomlClientEnvironment
+from phenoml.implementation_guides import FhirImplementationGuide
+
+client = PhenomlClient(
+ client_id="",
+ client_secret="",
+ environment=PhenomlClientEnvironment.DEFAULT,
+)
+
+client.implementation_guides.implementation_guides.create_version(
+ name="name",
+ implementation_guide=FhirImplementationGuide(
+ resource_type="ImplementationGuide",
+ url="url",
+ version="version",
+ ),
+ profile_refs=[
+ "profile_refs"
+ ],
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**name:** `str`
+
+
+
+
+
+-
+
+**implementation_guide:** `FhirImplementationGuide`
+
+
+
+
+
+-
+
+**profile_refs:** `typing.List[str]` — Exact canonical `url|version` references to builtin or custom profiles. A package can contain at most 250 references.
+
+
+
+
+
+-
+
+**profile_context:** `typing.Optional[str]` — Natural-language profile-selection context for this package.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
+
+
+
+
+client.implementation_guides.implementation_guides.get_version(...) -> ImplementationGuideVersionDetail
+
+-
+
+#### 🔌 Usage
+
+
+-
+
+
+-
+
+```python
+from phenoml import PhenomlClient
+from phenoml.environment import PhenomlClientEnvironment
+
+client = PhenomlClient(
+ client_id="",
+ client_secret="",
+ environment=PhenomlClientEnvironment.DEFAULT,
+)
+
+client.implementation_guides.implementation_guides.get_version(
+ name="name",
+ version="1.0.0",
+)
+
+```
+
+
+
+
+
+#### ⚙️ Parameters
+
+
+-
+
+
+-
+
+**name:** `str`
+
+
+
+
+
+-
+
+**version:** `str` — The authored ImplementationGuide.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.
+
+
+
+
+
+-
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
+
+
+
+
+
+
+
@@ -5540,10 +5716,8 @@ credential. A `request_id` whose job was canceled or failed before it
finalized is released for a fresh replay; once a job is finalized, its
`request_id` keeps resolving to it even after cancellation.
-An instance may hold at most 4 active (pending or processing) jobs at
-once; a create past that limit returns `409`. The limit is instance-wide
-— jobs are shared across the instance's credentials — so another
-credential's jobs count against it.
+There is no limit on how many jobs an instance may hold at once; how many
+items run in parallel is a property of the instance, not of the job count.
@@ -5880,8 +6054,8 @@ client.lang2fhir_batch.finalize(
-
-Drives a job to the terminal `canceled` state on request, freeing its
-active-job slot immediately. Takes no request body.
+Drives a job to the terminal `canceled` state on request. Takes no
+request body.
Cancel does not delete the job: the job record and any results already
produced are preserved for the normal retention window, the same as a
diff --git a/src/phenoml/agent/chat/client.py b/src/phenoml/agent/chat/client.py
index 9939bf6..6cce68f 100644
--- a/src/phenoml/agent/chat/client.py
+++ b/src/phenoml/agent/chat/client.py
@@ -418,7 +418,7 @@ async def stream(
async def main() -> None:
- response = await client.agent.chat.stream(
+ response = client.agent.chat.stream(
phenoml_on_behalf_of="Patient/550e8400-e29b-41d4-a716-446655440000",
phenoml_fhir_provider="550e8400-e29b-41d4-a716-446655440000:eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c...",
message="What is the patient's current condition?",
diff --git a/src/phenoml/agent/chat/raw_client.py b/src/phenoml/agent/chat/raw_client.py
index b163d97..221e657 100644
--- a/src/phenoml/agent/chat/raw_client.py
+++ b/src/phenoml/agent/chat/raw_client.py
@@ -277,6 +277,8 @@ def _iter():
for _sse in _event_source.iter_sse():
if _sse.data == None:
return
+ if len(_sse.data) == 0:
+ continue
try:
yield typing.cast(
AgentChatStreamEvent,
@@ -766,6 +768,8 @@ async def _iter():
async for _sse in _event_source.aiter_sse():
if _sse.data == None:
return
+ if len(_sse.data) == 0:
+ continue
try:
yield typing.cast(
AgentChatStreamEvent,
diff --git a/src/phenoml/client.py b/src/phenoml/client.py
index 87cfa7e..8073193 100644
--- a/src/phenoml/client.py
+++ b/src/phenoml/client.py
@@ -63,8 +63,8 @@ class PhenomlClient:
base_url : typing.Optional[str]
The base url to use for requests from the client.
- token : typing.Callable[[], str]
- Authenticate by providing a callable that returns a pre-generated bearer token. In this mode, OAuth client credentials are not required.
+ token : typing.Union[str, typing.Callable[[], str]]
+ Authenticate by providing a pre-generated bearer token, or a callable that returns one. In this mode, OAuth client credentials are not required.
timeout : typing.Optional[float]
The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced.
@@ -130,7 +130,7 @@ def __init__(
follow_redirects: typing.Optional[bool] = True,
httpx_client: typing.Optional[httpx.Client] = None,
logging: typing.Optional[typing.Union[LogConfig, Logger]] = None,
- token: typing.Callable[[], str],
+ token: typing.Union[str, typing.Callable[[], str]],
): ...
def __init__(
self,
@@ -141,7 +141,7 @@ def __init__(
headers: typing.Optional[typing.Dict[str, str]] = None,
client_id: typing.Optional[str] = os.getenv("PHENOML_CLIENT_ID"),
client_secret: typing.Optional[str] = os.getenv("PHENOML_CLIENT_SECRET"),
- token: typing.Optional[typing.Callable[[], str]] = None,
+ token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None,
_token_getter_override: typing.Optional[typing.Callable[[], str]] = None,
timeout: typing.Optional[float] = None,
max_retries: typing.Optional[int] = None,
@@ -155,7 +155,12 @@ def __init__(
_defaulted_max_retries = max_retries if max_retries is not None else 2
if instance_url is not None:
_instance_url = instance_url if instance_url is not None else "experiment.app.pheno.ml"
- base_url = "https://{instanceUrl}".format(instanceUrl=_instance_url)
+ _environment_url_templates = {
+ PhenomlClientEnvironment.DEFAULT: "https://{instanceUrl}",
+ }
+ _url_template = _environment_url_templates.get(environment, "https://{instanceUrl}")
+ if base_url is None:
+ base_url = _url_template.format(instanceUrl=_instance_url)
if token is not None:
self._client_wrapper = SyncClientWrapper(
base_url=_get_base_url(base_url=base_url, environment=environment),
@@ -398,8 +403,8 @@ class AsyncPhenomlClient:
base_url : typing.Optional[str]
The base url to use for requests from the client.
- token : typing.Callable[[], str]
- Authenticate by providing a callable that returns a pre-generated bearer token. In this mode, OAuth client credentials are not required.
+ token : typing.Union[str, typing.Callable[[], str]]
+ Authenticate by providing a pre-generated bearer token, or a callable that returns one. In this mode, OAuth client credentials are not required.
timeout : typing.Optional[float]
The timeout to be used, in seconds, for requests. By default the timeout is 60 seconds, unless a custom httpx client is used, in which case this default is not enforced.
@@ -465,7 +470,7 @@ def __init__(
follow_redirects: typing.Optional[bool] = True,
httpx_client: typing.Optional[httpx.AsyncClient] = None,
logging: typing.Optional[typing.Union[LogConfig, Logger]] = None,
- token: typing.Callable[[], str],
+ token: typing.Union[str, typing.Callable[[], str]],
): ...
def __init__(
self,
@@ -476,7 +481,7 @@ def __init__(
headers: typing.Optional[typing.Dict[str, str]] = None,
client_id: typing.Optional[str] = os.getenv("PHENOML_CLIENT_ID"),
client_secret: typing.Optional[str] = os.getenv("PHENOML_CLIENT_SECRET"),
- token: typing.Optional[typing.Callable[[], str]] = None,
+ token: typing.Optional[typing.Union[str, typing.Callable[[], str]]] = None,
_token_getter_override: typing.Optional[typing.Callable[[], str]] = None,
timeout: typing.Optional[float] = None,
max_retries: typing.Optional[int] = None,
@@ -490,7 +495,12 @@ def __init__(
_defaulted_max_retries = max_retries if max_retries is not None else 2
if instance_url is not None:
_instance_url = instance_url if instance_url is not None else "experiment.app.pheno.ml"
- base_url = "https://{instanceUrl}".format(instanceUrl=_instance_url)
+ _environment_url_templates = {
+ PhenomlClientEnvironment.DEFAULT: "https://{instanceUrl}",
+ }
+ _url_template = _environment_url_templates.get(environment, "https://{instanceUrl}")
+ if base_url is None:
+ base_url = _url_template.format(instanceUrl=_instance_url)
if token is not None:
self._client_wrapper = AsyncClientWrapper(
base_url=_get_base_url(base_url=base_url, environment=environment),
diff --git a/src/phenoml/core/client_wrapper.py b/src/phenoml/core/client_wrapper.py
index dc25c0d..c03a243 100644
--- a/src/phenoml/core/client_wrapper.py
+++ b/src/phenoml/core/client_wrapper.py
@@ -33,12 +33,12 @@ def get_headers(self) -> typing.Dict[str, str]:
import platform
headers: typing.Dict[str, str] = {
- "User-Agent": "phenoml/17.0.0",
+ "User-Agent": "phenoml/0.0.0-fern-placeholder",
"X-Fern-Language": "Python",
"X-Fern-Runtime": f"python/{platform.python_version()}",
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
"X-Fern-SDK-Name": "phenoml",
- "X-Fern-SDK-Version": "17.0.0",
+ "X-Fern-SDK-Version": "0.0.0.dev0",
**(self.get_custom_headers() or {}),
}
token = self._get_token()
diff --git a/src/phenoml/core/http_client.py b/src/phenoml/core/http_client.py
index 124dce2..3e5a786 100644
--- a/src/phenoml/core/http_client.py
+++ b/src/phenoml/core/http_client.py
@@ -3,6 +3,7 @@
import asyncio
import email.utils
import re
+import socket
import time
import typing
from contextlib import asynccontextmanager, contextmanager
@@ -23,6 +24,39 @@
JITTER_FACTOR = 0.2 # 20% random jitter
+def get_keepalive_socket_options(
+ idle: int = 60,
+ intvl: int = 30,
+ cnt: int = 5,
+) -> typing.List[typing.Tuple[int, int, int]]:
+ """
+ Build TCP keepalive socket options for the current platform.
+
+ Keepalive probes keep otherwise-idle connections alive so that long,
+ non-streaming requests survive idle-connection reaping by a firewall,
+ load balancer, or NAT. The available socket constants are OS-dependent,
+ so each option is guarded and only emitted when the platform defines it:
+
+ - ``SO_KEEPALIVE`` is portable (Linux/macOS/Windows).
+ - The idle-before-first-probe knob is ``TCP_KEEPIDLE`` on Linux and modern
+ Windows, but ``TCP_KEEPALIVE`` on macOS.
+ - ``TCP_KEEPINTVL`` / ``TCP_KEEPCNT`` exist on Linux/macOS/modern Windows.
+
+ Passing these tuples to ``httpx.HTTPTransport(socket_options=...)`` /
+ ``httpx.AsyncHTTPTransport(socket_options=...)`` applies them to every
+ connection the transport opens.
+ """
+ opts: typing.List[typing.Tuple[int, int, int]] = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)]
+ idle_const = getattr(socket, "TCP_KEEPIDLE", None) or getattr(socket, "TCP_KEEPALIVE", None)
+ if idle_const:
+ opts.append((socket.IPPROTO_TCP, idle_const, idle))
+ if hasattr(socket, "TCP_KEEPINTVL"):
+ opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPINTVL, intvl))
+ if hasattr(socket, "TCP_KEEPCNT"):
+ opts.append((socket.IPPROTO_TCP, socket.TCP_KEEPCNT, cnt))
+ return opts
+
+
def _parse_retry_after(response_headers: httpx.Headers) -> typing.Optional[float]:
"""
This function parses the `Retry-After` header in a HTTP response and returns the number of seconds to wait.
@@ -238,7 +272,16 @@ def get_request_body(
data: typing.Optional[typing.Any],
request_options: typing.Optional[RequestOptions],
omit: typing.Optional[typing.Any],
+ optional_body: bool = False,
) -> typing.Tuple[typing.Optional[typing.Any], typing.Optional[typing.Any]]:
+ # A whole body left at the sentinel was never passed by the caller, so it is absent
+ # rather than empty: the request carries no content and no `Content-Type`.
+ if omit is not None:
+ if json is omit:
+ json = None
+ if data is omit:
+ data = None
+
json_body = None
data_body = None
if data is not None:
@@ -254,14 +297,36 @@ def get_request_body(
# Only collapse empty dict to None when the body was not explicitly provided
# and there are no additional body parameters. This preserves explicit empty
# bodies (e.g., when an endpoint has a request body type but all fields are optional).
- if json_body == {} and json is None and not has_additional_body_parameters:
+ # `optional_body` marks an endpoint whose body the API does not require, where a body
+ # that ends up empty means the caller passed none of its properties, so the request is
+ # sent with no content and no `Content-Type`.
+ if json_body == {} and (json is None or optional_body) and not has_additional_body_parameters:
json_body = None
- if data_body == {} and data is None and not has_additional_body_parameters:
+ if data_body == {} and (data is None or optional_body) and not has_additional_body_parameters:
data_body = None
return json_body, data_body
+def drop_content_type_without_body(
+ headers: typing.Dict[str, typing.Any],
+ *,
+ json_body: typing.Optional[typing.Any],
+ data_body: typing.Optional[typing.Any],
+ optional_body: bool,
+) -> typing.Dict[str, typing.Any]:
+ """Strip ``Content-Type`` from a request that carries no body.
+
+ ``get_request_body`` drops the body of an ``optional_body`` endpoint when the caller
+ supplied none of it, but the endpoint still passes the content type it would have used.
+ A request that sends nothing must not advertise a media type, so a server that branches
+ on the header sees a bodyless call for what it is.
+ """
+ if not optional_body or json_body is not None or data_body is not None:
+ return headers
+ return {key: value for key, value in headers.items() if key.lower() != "content-type"}
+
+
class HttpClient:
def __init__(
self,
@@ -309,6 +374,7 @@ def request(
request_options: typing.Optional[RequestOptions] = None,
retries: int = 0,
omit: typing.Optional[typing.Any] = None,
+ optional_body: bool = False,
force_multipart: typing.Optional[bool] = None,
) -> httpx.Response:
base_url = self.get_base_url(base_url)
@@ -321,7 +387,9 @@ def request(
)
timeout = _timeout if _timeout is not None else httpx.USE_CLIENT_DEFAULT
- json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
+ json_body, data_body = get_request_body(
+ json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
+ )
request_files: typing.Optional[RequestFiles] = (
convert_file_dict_to_httpx_tuples(remove_omit_from_dict(remove_none_from_dict(files), omit))
@@ -364,6 +432,9 @@ def request(
}
)
)
+ _request_headers = drop_content_type_without_body(
+ _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
+ )
if self.logger.is_debug():
self.logger.debug(
@@ -472,6 +543,7 @@ def stream(
request_options: typing.Optional[RequestOptions] = None,
retries: int = 0,
omit: typing.Optional[typing.Any] = None,
+ optional_body: bool = False,
force_multipart: typing.Optional[bool] = None,
) -> typing.Iterator[httpx.Response]:
base_url = self.get_base_url(base_url)
@@ -493,7 +565,9 @@ def stream(
if (request_files is None or len(request_files) == 0) and force_multipart:
request_files = FORCE_MULTIPART
- json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
+ json_body, data_body = get_request_body(
+ json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
+ )
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
@@ -527,6 +601,9 @@ def stream(
}
)
)
+ _request_headers = drop_content_type_without_body(
+ _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
+ )
if self.logger.is_debug():
self.logger.debug(
@@ -604,6 +681,7 @@ async def request(
request_options: typing.Optional[RequestOptions] = None,
retries: int = 0,
omit: typing.Optional[typing.Any] = None,
+ optional_body: bool = False,
force_multipart: typing.Optional[bool] = None,
) -> httpx.Response:
base_url = self.get_base_url(base_url)
@@ -625,7 +703,9 @@ async def request(
if (request_files is None or len(request_files) == 0) and force_multipart:
request_files = FORCE_MULTIPART
- json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
+ json_body, data_body = get_request_body(
+ json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
+ )
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
@@ -662,6 +742,9 @@ async def request(
}
)
)
+ _request_headers = drop_content_type_without_body(
+ _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
+ )
if self.logger.is_debug():
self.logger.debug(
@@ -770,6 +853,7 @@ async def stream(
request_options: typing.Optional[RequestOptions] = None,
retries: int = 0,
omit: typing.Optional[typing.Any] = None,
+ optional_body: bool = False,
force_multipart: typing.Optional[bool] = None,
) -> typing.AsyncIterator[httpx.Response]:
base_url = self.get_base_url(base_url)
@@ -791,7 +875,9 @@ async def stream(
if (request_files is None or len(request_files) == 0) and force_multipart:
request_files = FORCE_MULTIPART
- json_body, data_body = get_request_body(json=json, data=data, request_options=request_options, omit=omit)
+ json_body, data_body = get_request_body(
+ json=json, data=data, request_options=request_options, omit=omit, optional_body=optional_body
+ )
data_body = _maybe_filter_none_from_multipart_data(data_body, request_files, force_multipart)
@@ -828,6 +914,9 @@ async def stream(
}
)
)
+ _request_headers = drop_content_type_without_body(
+ _request_headers, json_body=json_body, data_body=data_body, optional_body=optional_body
+ )
if self.logger.is_debug():
self.logger.debug(
diff --git a/src/phenoml/core/http_response.py b/src/phenoml/core/http_response.py
index 00bb109..9aa1e18 100644
--- a/src/phenoml/core/http_response.py
+++ b/src/phenoml/core/http_response.py
@@ -24,6 +24,10 @@ def headers(self) -> Dict[str, str]:
def status_code(self) -> int:
return self._response.status_code
+ @property
+ def response(self) -> httpx.Response:
+ return self._response
+
class HttpResponse(Generic[T], BaseHttpResponse):
"""HTTP response wrapper that exposes response headers and data."""
diff --git a/src/phenoml/core/jsonable_encoder.py b/src/phenoml/core/jsonable_encoder.py
index 5b0902e..f638cc9 100644
--- a/src/phenoml/core/jsonable_encoder.py
+++ b/src/phenoml/core/jsonable_encoder.py
@@ -15,6 +15,7 @@
from pathlib import PurePath
from types import GeneratorType
from typing import Any, Callable, Dict, List, Optional, Set, Union
+from urllib.parse import quote
import pydantic
from .datetime_utils import serialize_datetime
@@ -118,3 +119,15 @@ def encode_path_param(obj: Any) -> str:
if isinstance(obj, bool):
return "true" if obj else "false"
return str(jsonable_encoder(obj))
+
+
+def quote_path_param(obj: Any) -> str:
+ """Encode a value for use in a URL path segment, percent-encoding it.
+
+ Same as encode_path_param, except the result is percent-encoded so
+ that a value containing "/" or ".." cannot change which endpoint
+ the request resolves to.
+ """
+ if isinstance(obj, bool):
+ return "true" if obj else "false"
+ return quote(str(jsonable_encoder(obj)), safe="")
diff --git a/src/phenoml/core/oauth_token_provider.py b/src/phenoml/core/oauth_token_provider.py
index 0360d17..370bb86 100644
--- a/src/phenoml/core/oauth_token_provider.py
+++ b/src/phenoml/core/oauth_token_provider.py
@@ -31,7 +31,9 @@ def get_token(self) -> str:
return self._refresh()
def _refresh(self) -> str:
- token_response = self._auth_client.get_token(client_id=self._client_id, client_secret=self._client_secret)
+ token_response = self._auth_client.get_token(
+ client_id=self._client_id, client_secret=self._client_secret, grant_type="client_credentials"
+ )
self._access_token = token_response.access_token
self._expires_at = self._get_expires_at(
expires_in_seconds=token_response.expires_in, buffer_in_minutes=self.BUFFER_IN_MINUTES
@@ -62,7 +64,9 @@ async def get_token(self) -> str:
return await self._refresh()
async def _refresh(self) -> str:
- token_response = await self._auth_client.get_token(client_id=self._client_id, client_secret=self._client_secret)
+ token_response = await self._auth_client.get_token(
+ client_id=self._client_id, client_secret=self._client_secret, grant_type="client_credentials"
+ )
self._access_token = token_response.access_token
self._expires_at = self._get_expires_at(
expires_in_seconds=token_response.expires_in, buffer_in_minutes=self.BUFFER_IN_MINUTES
diff --git a/src/phenoml/core/pydantic_utilities.py b/src/phenoml/core/pydantic_utilities.py
index 6587f5e..70816b9 100644
--- a/src/phenoml/core/pydantic_utilities.py
+++ b/src/phenoml/core/pydantic_utilities.py
@@ -5,6 +5,7 @@
import inspect
import json
import logging
+import weakref
from collections import defaultdict
from dataclasses import asdict
from typing import (
@@ -184,6 +185,58 @@ def _get_type_adapter(type_: Type[Any]) -> Any:
return adapter
+_field_alias_cache: "weakref.WeakKeyDictionary[type, Tuple[Dict[str, str], Tuple[str, ...]]]" = (
+ weakref.WeakKeyDictionary()
+)
+
+
+def _get_field_aliases(model: type) -> Tuple[Dict[str, str], Tuple[str, ...]]:
+ """
+ Map of field name to Pydantic alias for the fields whose alias differs from their name, together with the
+ keys that are ambiguous (an alias of one field and the name of another). Computed once per model class.
+ """
+ cached = _field_alias_cache.get(model)
+ if cached is None:
+ fields: Mapping[str, Any] = (
+ getattr(model, "model_fields", {}) if IS_PYDANTIC_V2 else getattr(model, "__fields__", {})
+ )
+ name_to_alias: Dict[str, str] = {}
+ for name, field in fields.items():
+ alias = getattr(field, "alias", None)
+ if alias is not None and alias != name:
+ name_to_alias[name] = alias
+ cached = (name_to_alias, tuple(alias for alias in name_to_alias.values() if alias in fields))
+ _field_alias_cache[model] = cached
+ return cached
+
+
+def _coerce_keys_to_aliases(model: type, data: Any) -> Any:
+ """
+ Accept Python field names in input by rewriting them to their Pydantic aliases,
+ while avoiding silent collisions when a key could refer to multiple fields.
+ """
+ if not isinstance(data, Mapping):
+ return data
+
+ name_to_alias, ambiguous_keys = _get_field_aliases(model)
+ for key in ambiguous_keys:
+ if key in data and name_to_alias.get(key, key) not in data:
+ raise ValueError(
+ f"Ambiguous input key '{key}': it is both a field name and an alias. "
+ "Provide the explicit alias key to disambiguate."
+ )
+
+ if not name_to_alias or not any(name in data for name in name_to_alias):
+ return data if isinstance(data, dict) else dict(data)
+
+ rewritten: Dict[str, Any] = dict(data)
+ for name, alias in name_to_alias.items():
+ if name in data and alias not in rewritten:
+ rewritten[alias] = rewritten.pop(name)
+
+ return rewritten
+
+
def parse_obj_as(type_: Type[T], object_: Any) -> T:
# convert_and_respect_annotation_metadata is required for TypedDict aliasing.
#
@@ -193,20 +246,7 @@ def parse_obj_as(type_: Type[T], object_: Any) -> T:
# - If the model encodes aliasing only via FieldMetadata annotations, then we MUST pre-dealias because Pydantic
# will not recognize those aliases during validation.
if inspect.isclass(type_) and issubclass(type_, pydantic.BaseModel):
- has_pydantic_aliases = False
- if IS_PYDANTIC_V2:
- for field_name, field_info in getattr(type_, "model_fields", {}).items(): # type: ignore[attr-defined]
- alias = getattr(field_info, "alias", None)
- if alias is not None and alias != field_name:
- has_pydantic_aliases = True
- break
- else:
- for field in getattr(type_, "__fields__", {}).values():
- alias = getattr(field, "alias", None)
- name = getattr(field, "name", None)
- if alias is not None and name is not None and alias != name:
- has_pydantic_aliases = True
- break
+ has_pydantic_aliases = bool(_get_field_aliases(type_)[0])
dealiased_object = (
object_
@@ -239,39 +279,7 @@ class UniversalBaseModel(pydantic.BaseModel):
@pydantic.model_validator(mode="before") # type: ignore[attr-defined]
@classmethod
def _coerce_field_names_to_aliases(cls, data: Any) -> Any:
- """
- Accept Python field names in input by rewriting them to their Pydantic aliases,
- while avoiding silent collisions when a key could refer to multiple fields.
- """
- if not isinstance(data, Mapping):
- return data
-
- fields = getattr(cls, "model_fields", {}) # type: ignore[attr-defined]
- name_to_alias: Dict[str, str] = {}
- alias_to_name: Dict[str, str] = {}
-
- for name, field_info in fields.items():
- alias = getattr(field_info, "alias", None) or name
- name_to_alias[name] = alias
- if alias != name:
- alias_to_name[alias] = name
-
- # Detect ambiguous keys: a key that is an alias for one field and a name for another.
- ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys()))
- for key in ambiguous_keys:
- if key in data and name_to_alias[key] not in data:
- raise ValueError(
- f"Ambiguous input key '{key}': it is both a field name and an alias. "
- "Provide the explicit alias key to disambiguate."
- )
-
- original_keys = set(data.keys())
- rewritten: Dict[str, Any] = dict(data)
- for name, alias in name_to_alias.items():
- if alias != name and name in original_keys and alias not in rewritten:
- rewritten[alias] = rewritten.pop(name)
-
- return rewritten
+ return _coerce_keys_to_aliases(cls, data)
@pydantic.model_serializer(mode="plain", when_used="json") # type: ignore[attr-defined]
def serialize_model(self) -> Any: # type: ignore[name-defined]
@@ -287,37 +295,7 @@ class Config:
@pydantic.root_validator(pre=True)
def _coerce_field_names_to_aliases(cls, values: Any) -> Any:
- """
- Pydantic v1 equivalent of _coerce_field_names_to_aliases.
- """
- if not isinstance(values, Mapping):
- return values
-
- fields = getattr(cls, "__fields__", {})
- name_to_alias: Dict[str, str] = {}
- alias_to_name: Dict[str, str] = {}
-
- for name, field in fields.items():
- alias = getattr(field, "alias", None) or name
- name_to_alias[name] = alias
- if alias != name:
- alias_to_name[alias] = name
-
- ambiguous_keys = set(alias_to_name.keys()).intersection(set(name_to_alias.keys()))
- for key in ambiguous_keys:
- if key in values and name_to_alias[key] not in values:
- raise ValueError(
- f"Ambiguous input key '{key}': it is both a field name and an alias. "
- "Provide the explicit alias key to disambiguate."
- )
-
- original_keys = set(values.keys())
- rewritten: Dict[str, Any] = dict(values)
- for name, alias in name_to_alias.items():
- if alias != name and name in original_keys and alias not in rewritten:
- rewritten[alias] = rewritten.pop(name)
-
- return rewritten
+ return _coerce_keys_to_aliases(cls, values) # type: ignore[arg-type]
@classmethod
def model_construct(cls: Type["Model"], _fields_set: Optional[Set[str]] = None, **values: Any) -> "Model":
diff --git a/src/phenoml/implementation_guides/__init__.py b/src/phenoml/implementation_guides/__init__.py
index 2d19295..8b08ecc 100644
--- a/src/phenoml/implementation_guides/__init__.py
+++ b/src/phenoml/implementation_guides/__init__.py
@@ -6,15 +6,31 @@
from importlib import import_module
if typing.TYPE_CHECKING:
- from .types import ImplementationGuideDetail, ImplementationGuideListResponse, ImplementationGuideSummary
- from .errors import BadRequestError, ForbiddenError, InternalServerError, NotFoundError, UnauthorizedError
+ from .types import (
+ FhirImplementationGuide,
+ ImplementationGuideDetail,
+ ImplementationGuideListResponse,
+ ImplementationGuideSummary,
+ ImplementationGuideVersionDetail,
+ )
+ from .errors import (
+ BadRequestError,
+ ConflictError,
+ ForbiddenError,
+ InternalServerError,
+ NotFoundError,
+ UnauthorizedError,
+ )
from . import implementation_guides
_dynamic_imports: typing.Dict[str, str] = {
"BadRequestError": ".errors",
+ "ConflictError": ".errors",
+ "FhirImplementationGuide": ".types",
"ForbiddenError": ".errors",
"ImplementationGuideDetail": ".types",
"ImplementationGuideListResponse": ".types",
"ImplementationGuideSummary": ".types",
+ "ImplementationGuideVersionDetail": ".types",
"InternalServerError": ".errors",
"NotFoundError": ".errors",
"UnauthorizedError": ".errors",
@@ -45,10 +61,13 @@ def __dir__():
__all__ = [
"BadRequestError",
+ "ConflictError",
+ "FhirImplementationGuide",
"ForbiddenError",
"ImplementationGuideDetail",
"ImplementationGuideListResponse",
"ImplementationGuideSummary",
+ "ImplementationGuideVersionDetail",
"InternalServerError",
"NotFoundError",
"UnauthorizedError",
diff --git a/src/phenoml/implementation_guides/errors/__init__.py b/src/phenoml/implementation_guides/errors/__init__.py
index 306a114..29aa057 100644
--- a/src/phenoml/implementation_guides/errors/__init__.py
+++ b/src/phenoml/implementation_guides/errors/__init__.py
@@ -7,12 +7,14 @@
if typing.TYPE_CHECKING:
from .bad_request_error import BadRequestError
+ from .conflict_error import ConflictError
from .forbidden_error import ForbiddenError
from .internal_server_error import InternalServerError
from .not_found_error import NotFoundError
from .unauthorized_error import UnauthorizedError
_dynamic_imports: typing.Dict[str, str] = {
"BadRequestError": ".bad_request_error",
+ "ConflictError": ".conflict_error",
"ForbiddenError": ".forbidden_error",
"InternalServerError": ".internal_server_error",
"NotFoundError": ".not_found_error",
@@ -41,4 +43,11 @@ def __dir__():
return sorted(lazy_attrs)
-__all__ = ["BadRequestError", "ForbiddenError", "InternalServerError", "NotFoundError", "UnauthorizedError"]
+__all__ = [
+ "BadRequestError",
+ "ConflictError",
+ "ForbiddenError",
+ "InternalServerError",
+ "NotFoundError",
+ "UnauthorizedError",
+]
diff --git a/src/phenoml/implementation_guides/errors/conflict_error.py b/src/phenoml/implementation_guides/errors/conflict_error.py
new file mode 100644
index 0000000..abe1a9f
--- /dev/null
+++ b/src/phenoml/implementation_guides/errors/conflict_error.py
@@ -0,0 +1,10 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+from ...core.api_error import ApiError
+
+
+class ConflictError(ApiError):
+ def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None):
+ super().__init__(status_code=409, headers=headers, body=body)
diff --git a/src/phenoml/implementation_guides/implementation_guides/client.py b/src/phenoml/implementation_guides/implementation_guides/client.py
index 6cc3374..ba604eb 100644
--- a/src/phenoml/implementation_guides/implementation_guides/client.py
+++ b/src/phenoml/implementation_guides/implementation_guides/client.py
@@ -4,9 +4,11 @@
from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper
from ...core.request_options import RequestOptions
+from ..types.fhir_implementation_guide import FhirImplementationGuide
from ..types.implementation_guide_detail import ImplementationGuideDetail
from ..types.implementation_guide_list_response import ImplementationGuideListResponse
from ..types.implementation_guide_summary import ImplementationGuideSummary
+from ..types.implementation_guide_version_detail import ImplementationGuideVersionDetail
from .raw_client import AsyncRawImplementationGuidesClient, RawImplementationGuidesClient
# this is used as the default value for optional parameters
@@ -136,11 +138,9 @@ def update(
def delete(self, name: str, *, request_options: typing.Optional[RequestOptions] = None) -> None:
"""
- Deletes the stored metadata for an implementation guide — its
- profile_context and timestamps. Member profiles keep their
- implementation_guide assignment, so a guide still referenced by at least
- one profile continues to appear in listings, just without context or
- timestamps.
+ Deletes the stored name-level metadata and any exact canonical package
+ versions beneath the guide. Legacy member profile assignments are not
+ changed.
Parameters
----------
@@ -169,6 +169,102 @@ def delete(self, name: str, *, request_options: typing.Optional[RequestOptions]
_response = self._raw_client.delete(name, request_options=request_options)
return _response.data
+ def create_version(
+ self,
+ name: str,
+ *,
+ implementation_guide: FhirImplementationGuide,
+ profile_refs: typing.Sequence[str],
+ profile_context: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> ImplementationGuideVersionDetail:
+ """
+ Publishes an exact package beneath this guide family. PR 2 temporarily
+ permits one exact package version per guide family; publishing another
+ version returns `409 Conflict` until multi-version package support lands.
+
+ Parameters
+ ----------
+ name : str
+
+ implementation_guide : FhirImplementationGuide
+
+ profile_refs : typing.Sequence[str]
+ Exact canonical `url|version` references to builtin or custom profiles. A package can contain at most 250 references.
+
+ profile_context : typing.Optional[str]
+ Natural-language profile-selection context for this package.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ImplementationGuideVersionDetail
+ Canonical package published
+
+ Examples
+ --------
+ from phenoml import PhenomlClient
+ from phenoml.implementation_guides import FhirImplementationGuide
+
+ client = PhenomlClient(
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ )
+ client.implementation_guides.implementation_guides.create_version(
+ name="name",
+ implementation_guide=FhirImplementationGuide(
+ url="url",
+ version="version",
+ ),
+ profile_refs=["profile_refs"],
+ )
+ """
+ _response = self._raw_client.create_version(
+ name,
+ implementation_guide=implementation_guide,
+ profile_refs=profile_refs,
+ profile_context=profile_context,
+ request_options=request_options,
+ )
+ return _response.data
+
+ def get_version(
+ self, name: str, version: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> ImplementationGuideVersionDetail:
+ """
+ Parameters
+ ----------
+ name : str
+
+ version : str
+ The authored ImplementationGuide.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ImplementationGuideVersionDetail
+ Exact canonical package
+
+ Examples
+ --------
+ from phenoml import PhenomlClient
+
+ client = PhenomlClient(
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ )
+ client.implementation_guides.implementation_guides.get_version(
+ name="name",
+ version="1.0.0",
+ )
+ """
+ _response = self._raw_client.get_version(name, version, request_options=request_options)
+ return _response.data
+
class AsyncImplementationGuidesClient:
def __init__(self, *, client_wrapper: AsyncClientWrapper):
@@ -321,11 +417,9 @@ async def main() -> None:
async def delete(self, name: str, *, request_options: typing.Optional[RequestOptions] = None) -> None:
"""
- Deletes the stored metadata for an implementation guide — its
- profile_context and timestamps. Member profiles keep their
- implementation_guide assignment, so a guide still referenced by at least
- one profile continues to appear in listings, just without context or
- timestamps.
+ Deletes the stored name-level metadata and any exact canonical package
+ versions beneath the guide. Legacy member profile assignments are not
+ changed.
Parameters
----------
@@ -361,3 +455,115 @@ async def main() -> None:
"""
_response = await self._raw_client.delete(name, request_options=request_options)
return _response.data
+
+ async def create_version(
+ self,
+ name: str,
+ *,
+ implementation_guide: FhirImplementationGuide,
+ profile_refs: typing.Sequence[str],
+ profile_context: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> ImplementationGuideVersionDetail:
+ """
+ Publishes an exact package beneath this guide family. PR 2 temporarily
+ permits one exact package version per guide family; publishing another
+ version returns `409 Conflict` until multi-version package support lands.
+
+ Parameters
+ ----------
+ name : str
+
+ implementation_guide : FhirImplementationGuide
+
+ profile_refs : typing.Sequence[str]
+ Exact canonical `url|version` references to builtin or custom profiles. A package can contain at most 250 references.
+
+ profile_context : typing.Optional[str]
+ Natural-language profile-selection context for this package.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ImplementationGuideVersionDetail
+ Canonical package published
+
+ Examples
+ --------
+ import asyncio
+
+ from phenoml import AsyncPhenomlClient
+ from phenoml.implementation_guides import FhirImplementationGuide
+
+ client = AsyncPhenomlClient(
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ )
+
+
+ async def main() -> None:
+ await client.implementation_guides.implementation_guides.create_version(
+ name="name",
+ implementation_guide=FhirImplementationGuide(
+ url="url",
+ version="version",
+ ),
+ profile_refs=["profile_refs"],
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.create_version(
+ name,
+ implementation_guide=implementation_guide,
+ profile_refs=profile_refs,
+ profile_context=profile_context,
+ request_options=request_options,
+ )
+ return _response.data
+
+ async def get_version(
+ self, name: str, version: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> ImplementationGuideVersionDetail:
+ """
+ Parameters
+ ----------
+ name : str
+
+ version : str
+ The authored ImplementationGuide.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ ImplementationGuideVersionDetail
+ Exact canonical package
+
+ Examples
+ --------
+ import asyncio
+
+ from phenoml import AsyncPhenomlClient
+
+ client = AsyncPhenomlClient(
+ client_id="YOUR_CLIENT_ID",
+ client_secret="YOUR_CLIENT_SECRET",
+ )
+
+
+ async def main() -> None:
+ await client.implementation_guides.implementation_guides.get_version(
+ name="name",
+ version="1.0.0",
+ )
+
+
+ asyncio.run(main())
+ """
+ _response = await self._raw_client.get_version(name, version, request_options=request_options)
+ return _response.data
diff --git a/src/phenoml/implementation_guides/implementation_guides/raw_client.py b/src/phenoml/implementation_guides/implementation_guides/raw_client.py
index 8700e84..1eb8291 100644
--- a/src/phenoml/implementation_guides/implementation_guides/raw_client.py
+++ b/src/phenoml/implementation_guides/implementation_guides/raw_client.py
@@ -10,14 +10,18 @@
from ...core.parse_error import ParsingError
from ...core.pydantic_utilities import parse_obj_as
from ...core.request_options import RequestOptions
+from ...core.serialization import convert_and_respect_annotation_metadata
from ..errors.bad_request_error import BadRequestError
+from ..errors.conflict_error import ConflictError
from ..errors.forbidden_error import ForbiddenError
from ..errors.internal_server_error import InternalServerError
from ..errors.not_found_error import NotFoundError
from ..errors.unauthorized_error import UnauthorizedError
+from ..types.fhir_implementation_guide import FhirImplementationGuide
from ..types.implementation_guide_detail import ImplementationGuideDetail
from ..types.implementation_guide_list_response import ImplementationGuideListResponse
from ..types.implementation_guide_summary import ImplementationGuideSummary
+from ..types.implementation_guide_version_detail import ImplementationGuideVersionDetail
from pydantic import ValidationError
# this is used as the default value for optional parameters
@@ -308,11 +312,9 @@ def update(
def delete(self, name: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[None]:
"""
- Deletes the stored metadata for an implementation guide — its
- profile_context and timestamps. Member profiles keep their
- implementation_guide assignment, so a guide still referenced by at least
- one profile continues to appear in listings, just without context or
- timestamps.
+ Deletes the stored name-level metadata and any exact canonical package
+ versions beneath the guide. Legacy member profile assignments are not
+ changed.
Parameters
----------
@@ -398,6 +400,173 @@ def delete(self, name: str, *, request_options: typing.Optional[RequestOptions]
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+ def create_version(
+ self,
+ name: str,
+ *,
+ implementation_guide: FhirImplementationGuide,
+ profile_refs: typing.Sequence[str],
+ profile_context: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> HttpResponse[ImplementationGuideVersionDetail]:
+ """
+ Publishes an exact package beneath this guide family. PR 2 temporarily
+ permits one exact package version per guide family; publishing another
+ version returns `409 Conflict` until multi-version package support lands.
+
+ Parameters
+ ----------
+ name : str
+
+ implementation_guide : FhirImplementationGuide
+
+ profile_refs : typing.Sequence[str]
+ Exact canonical `url|version` references to builtin or custom profiles. A package can contain at most 250 references.
+
+ profile_context : typing.Optional[str]
+ Natural-language profile-selection context for this package.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[ImplementationGuideVersionDetail]
+ Canonical package published
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ f"fhir/implementation-guides/{encode_path_param(name)}/versions",
+ method="POST",
+ json={
+ "implementation_guide": convert_and_respect_annotation_metadata(
+ object_=implementation_guide, annotation=FhirImplementationGuide, direction="write"
+ ),
+ "profile_refs": profile_refs,
+ "profile_context": profile_context,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ImplementationGuideVersionDetail,
+ parse_obj_as(
+ type_=ImplementationGuideVersionDetail, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 409:
+ raise ConflictError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ def get_version(
+ self, name: str, version: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> HttpResponse[ImplementationGuideVersionDetail]:
+ """
+ Parameters
+ ----------
+ name : str
+
+ version : str
+ The authored ImplementationGuide.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ HttpResponse[ImplementationGuideVersionDetail]
+ Exact canonical package
+ """
+ _response = self._client_wrapper.httpx_client.request(
+ f"fhir/implementation-guides/{encode_path_param(name)}/versions/{encode_path_param(version)}",
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ImplementationGuideVersionDetail,
+ parse_obj_as(
+ type_=ImplementationGuideVersionDetail, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return HttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
class AsyncRawImplementationGuidesClient:
def __init__(self, *, client_wrapper: AsyncClientWrapper):
@@ -685,11 +854,9 @@ async def delete(
self, name: str, *, request_options: typing.Optional[RequestOptions] = None
) -> AsyncHttpResponse[None]:
"""
- Deletes the stored metadata for an implementation guide — its
- profile_context and timestamps. Member profiles keep their
- implementation_guide assignment, so a guide still referenced by at least
- one profile continues to appear in listings, just without context or
- timestamps.
+ Deletes the stored name-level metadata and any exact canonical package
+ versions beneath the guide. Legacy member profile assignments are not
+ changed.
Parameters
----------
@@ -774,3 +941,170 @@ async def delete(
status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
)
raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def create_version(
+ self,
+ name: str,
+ *,
+ implementation_guide: FhirImplementationGuide,
+ profile_refs: typing.Sequence[str],
+ profile_context: typing.Optional[str] = OMIT,
+ request_options: typing.Optional[RequestOptions] = None,
+ ) -> AsyncHttpResponse[ImplementationGuideVersionDetail]:
+ """
+ Publishes an exact package beneath this guide family. PR 2 temporarily
+ permits one exact package version per guide family; publishing another
+ version returns `409 Conflict` until multi-version package support lands.
+
+ Parameters
+ ----------
+ name : str
+
+ implementation_guide : FhirImplementationGuide
+
+ profile_refs : typing.Sequence[str]
+ Exact canonical `url|version` references to builtin or custom profiles. A package can contain at most 250 references.
+
+ profile_context : typing.Optional[str]
+ Natural-language profile-selection context for this package.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[ImplementationGuideVersionDetail]
+ Canonical package published
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ f"fhir/implementation-guides/{encode_path_param(name)}/versions",
+ method="POST",
+ json={
+ "implementation_guide": convert_and_respect_annotation_metadata(
+ object_=implementation_guide, annotation=FhirImplementationGuide, direction="write"
+ ),
+ "profile_refs": profile_refs,
+ "profile_context": profile_context,
+ },
+ headers={
+ "content-type": "application/json",
+ },
+ request_options=request_options,
+ omit=OMIT,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ImplementationGuideVersionDetail,
+ parse_obj_as(
+ type_=ImplementationGuideVersionDetail, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 409:
+ raise ConflictError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
+
+ async def get_version(
+ self, name: str, version: str, *, request_options: typing.Optional[RequestOptions] = None
+ ) -> AsyncHttpResponse[ImplementationGuideVersionDetail]:
+ """
+ Parameters
+ ----------
+ name : str
+
+ version : str
+ The authored ImplementationGuide.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.
+
+ request_options : typing.Optional[RequestOptions]
+ Request-specific configuration.
+
+ Returns
+ -------
+ AsyncHttpResponse[ImplementationGuideVersionDetail]
+ Exact canonical package
+ """
+ _response = await self._client_wrapper.httpx_client.request(
+ f"fhir/implementation-guides/{encode_path_param(name)}/versions/{encode_path_param(version)}",
+ method="GET",
+ request_options=request_options,
+ )
+ try:
+ if 200 <= _response.status_code < 300:
+ _data = typing.cast(
+ ImplementationGuideVersionDetail,
+ parse_obj_as(
+ type_=ImplementationGuideVersionDetail, # type: ignore
+ object_=_response.json(),
+ ),
+ )
+ return AsyncHttpResponse(response=_response, data=_data)
+ if _response.status_code == 400:
+ raise BadRequestError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ if _response.status_code == 404:
+ raise NotFoundError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
+ _response_json = _response.json()
+ except JSONDecodeError:
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response.text)
+ except ValidationError as e:
+ raise ParsingError(
+ status_code=_response.status_code, headers=dict(_response.headers), body=_response.json(), cause=e
+ )
+ raise ApiError(status_code=_response.status_code, headers=dict(_response.headers), body=_response_json)
diff --git a/src/phenoml/implementation_guides/types/__init__.py b/src/phenoml/implementation_guides/types/__init__.py
index 2b67df5..6c2a89f 100644
--- a/src/phenoml/implementation_guides/types/__init__.py
+++ b/src/phenoml/implementation_guides/types/__init__.py
@@ -6,13 +6,17 @@
from importlib import import_module
if typing.TYPE_CHECKING:
+ from .fhir_implementation_guide import FhirImplementationGuide
from .implementation_guide_detail import ImplementationGuideDetail
from .implementation_guide_list_response import ImplementationGuideListResponse
from .implementation_guide_summary import ImplementationGuideSummary
+ from .implementation_guide_version_detail import ImplementationGuideVersionDetail
_dynamic_imports: typing.Dict[str, str] = {
+ "FhirImplementationGuide": ".fhir_implementation_guide",
"ImplementationGuideDetail": ".implementation_guide_detail",
"ImplementationGuideListResponse": ".implementation_guide_list_response",
"ImplementationGuideSummary": ".implementation_guide_summary",
+ "ImplementationGuideVersionDetail": ".implementation_guide_version_detail",
}
@@ -37,4 +41,10 @@ def __dir__():
return sorted(lazy_attrs)
-__all__ = ["ImplementationGuideDetail", "ImplementationGuideListResponse", "ImplementationGuideSummary"]
+__all__ = [
+ "FhirImplementationGuide",
+ "ImplementationGuideDetail",
+ "ImplementationGuideListResponse",
+ "ImplementationGuideSummary",
+ "ImplementationGuideVersionDetail",
+]
diff --git a/src/phenoml/implementation_guides/types/fhir_implementation_guide.py b/src/phenoml/implementation_guides/types/fhir_implementation_guide.py
new file mode 100644
index 0000000..949b9df
--- /dev/null
+++ b/src/phenoml/implementation_guides/types/fhir_implementation_guide.py
@@ -0,0 +1,38 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import typing
+
+import pydantic
+import typing_extensions
+from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from ...core.serialization import FieldMetadata
+
+
+class FhirImplementationGuide(UniversalBaseModel):
+ """
+ A complete authored FHIR ImplementationGuide JSON resource.
+ """
+
+ resource_type: typing_extensions.Annotated[
+ typing.Literal["ImplementationGuide"], FieldMetadata(alias="resourceType"), pydantic.Field(alias="resourceType")
+ ] = "ImplementationGuide"
+ id: typing.Optional[str] = None
+ url: str
+ version: str
+ name: typing.Optional[str] = None
+ status: typing.Optional[str] = None
+ package_id: typing_extensions.Annotated[
+ typing.Optional[str], FieldMetadata(alias="packageId"), pydantic.Field(alias="packageId")
+ ] = None
+ fhir_version: typing_extensions.Annotated[
+ typing.Optional[typing.List[str]], FieldMetadata(alias="fhirVersion"), pydantic.Field(alias="fhirVersion")
+ ] = None
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/phenoml/implementation_guides/types/implementation_guide_summary.py b/src/phenoml/implementation_guides/types/implementation_guide_summary.py
index bd3364d..a6489ab 100644
--- a/src/phenoml/implementation_guides/types/implementation_guide_summary.py
+++ b/src/phenoml/implementation_guides/types/implementation_guide_summary.py
@@ -9,7 +9,7 @@
class ImplementationGuideSummary(UniversalBaseModel):
"""
- Metadata for an implementation guide. This is an instance-local grouping record, not a complete FHIR ImplementationGuide resource.
+ Metadata for an implementation guide. Canonical fields are present only for published canonical packages; metadata-only legacy records omit them.
"""
name: typing.Optional[str] = pydantic.Field(default=None)
@@ -27,6 +27,16 @@ class ImplementationGuideSummary(UniversalBaseModel):
The number of custom profiles in this implementation guide.
"""
+ canonical_url: typing.Optional[str] = pydantic.Field(default=None)
+ """
+ Canonical FHIR ImplementationGuide URL, when the family has an exact package.
+ """
+
+ version_count: typing.Optional[int] = pydantic.Field(default=None)
+ """
+ Number of retained exact package versions.
+ """
+
created_at: typing.Optional[dt.datetime] = pydantic.Field(default=None)
"""
Present only for guides that have stored metadata (a profile_context has been set). Omitted for guides that exist solely because a profile references them.
diff --git a/src/phenoml/implementation_guides/types/implementation_guide_version_detail.py b/src/phenoml/implementation_guides/types/implementation_guide_version_detail.py
new file mode 100644
index 0000000..a5c2e66
--- /dev/null
+++ b/src/phenoml/implementation_guides/types/implementation_guide_version_detail.py
@@ -0,0 +1,29 @@
+# This file was auto-generated by Fern from our API Definition.
+
+import datetime as dt
+import typing
+
+import pydantic
+from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel
+from .fhir_implementation_guide import FhirImplementationGuide
+
+
+class ImplementationGuideVersionDetail(UniversalBaseModel):
+ name: str
+ url: str
+ version: str
+ profile_context: str
+ profiles: typing.List[str]
+ profile_refs: typing.List[str]
+ implementation_guide: FhirImplementationGuide
+ created_at: dt.datetime
+ updated_at: dt.datetime
+
+ if IS_PYDANTIC_V2:
+ model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2
+ else:
+
+ class Config:
+ frozen = True
+ smart_union = True
+ extra = pydantic.Extra.allow
diff --git a/src/phenoml/lang2fhir_batch/client.py b/src/phenoml/lang2fhir_batch/client.py
index d641755..3f8457a 100644
--- a/src/phenoml/lang2fhir_batch/client.py
+++ b/src/phenoml/lang2fhir_batch/client.py
@@ -90,10 +90,8 @@ def create(
finalized is released for a fresh replay; once a job is finalized, its
`request_id` keeps resolving to it even after cancellation.
- An instance may hold at most 4 active (pending or processing) jobs at
- once; a create past that limit returns `409`. The limit is instance-wide
- — jobs are shared across the instance's credentials — so another
- credential's jobs count against it.
+ There is no limit on how many jobs an instance may hold at once; how many
+ items run in parallel is a property of the instance, not of the job count.
Parameters
----------
@@ -281,8 +279,8 @@ def finalize(self, job_id: str, *, request_options: typing.Optional[RequestOptio
def cancel(self, job_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> BatchJob:
"""
- Drives a job to the terminal `canceled` state on request, freeing its
- active-job slot immediately. Takes no request body.
+ Drives a job to the terminal `canceled` state on request. Takes no
+ request body.
Cancel does not delete the job: the job record and any results already
produced are preserved for the normal retention window, the same as a
@@ -554,10 +552,8 @@ async def create(
finalized is released for a fresh replay; once a job is finalized, its
`request_id` keeps resolving to it even after cancellation.
- An instance may hold at most 4 active (pending or processing) jobs at
- once; a create past that limit returns `409`. The limit is instance-wide
- — jobs are shared across the instance's credentials — so another
- credential's jobs count against it.
+ There is no limit on how many jobs an instance may hold at once; how many
+ items run in parallel is a property of the instance, not of the job count.
Parameters
----------
@@ -769,8 +765,8 @@ async def main() -> None:
async def cancel(self, job_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> BatchJob:
"""
- Drives a job to the terminal `canceled` state on request, freeing its
- active-job slot immediately. Takes no request body.
+ Drives a job to the terminal `canceled` state on request. Takes no
+ request body.
Cancel does not delete the job: the job record and any results already
produced are preserved for the normal retention window, the same as a
diff --git a/src/phenoml/lang2fhir_batch/raw_client.py b/src/phenoml/lang2fhir_batch/raw_client.py
index 8873fac..26fc905 100644
--- a/src/phenoml/lang2fhir_batch/raw_client.py
+++ b/src/phenoml/lang2fhir_batch/raw_client.py
@@ -161,10 +161,8 @@ def create(
finalized is released for a fresh replay; once a job is finalized, its
`request_id` keeps resolving to it even after cancellation.
- An instance may hold at most 4 active (pending or processing) jobs at
- once; a create past that limit returns `409`. The limit is instance-wide
- — jobs are shared across the instance's credentials — so another
- credential's jobs count against it.
+ There is no limit on how many jobs an instance may hold at once; how many
+ items run in parallel is a property of the instance, not of the job count.
Parameters
----------
@@ -224,17 +222,6 @@ def create(
),
),
)
- if _response.status_code == 409:
- raise ConflictError(
- headers=dict(_response.headers),
- body=typing.cast(
- typing.Any,
- parse_obj_as(
- type_=typing.Any, # type: ignore
- object_=_response.json(),
- ),
- ),
- )
if _response.status_code == 499:
raise ClientClosedRequestError(
headers=dict(_response.headers),
@@ -630,8 +617,8 @@ def finalize(
def cancel(self, job_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[BatchJob]:
"""
- Drives a job to the terminal `canceled` state on request, freeing its
- active-job slot immediately. Takes no request body.
+ Drives a job to the terminal `canceled` state on request. Takes no
+ request body.
Cancel does not delete the job: the job record and any results already
produced are preserved for the normal retention window, the same as a
@@ -1265,10 +1252,8 @@ async def create(
finalized is released for a fresh replay; once a job is finalized, its
`request_id` keeps resolving to it even after cancellation.
- An instance may hold at most 4 active (pending or processing) jobs at
- once; a create past that limit returns `409`. The limit is instance-wide
- — jobs are shared across the instance's credentials — so another
- credential's jobs count against it.
+ There is no limit on how many jobs an instance may hold at once; how many
+ items run in parallel is a property of the instance, not of the job count.
Parameters
----------
@@ -1328,17 +1313,6 @@ async def create(
),
),
)
- if _response.status_code == 409:
- raise ConflictError(
- headers=dict(_response.headers),
- body=typing.cast(
- typing.Any,
- parse_obj_as(
- type_=typing.Any, # type: ignore
- object_=_response.json(),
- ),
- ),
- )
if _response.status_code == 499:
raise ClientClosedRequestError(
headers=dict(_response.headers),
@@ -1736,8 +1710,8 @@ async def cancel(
self, job_id: str, *, request_options: typing.Optional[RequestOptions] = None
) -> AsyncHttpResponse[BatchJob]:
"""
- Drives a job to the terminal `canceled` state on request, freeing its
- active-job slot immediately. Takes no request body.
+ Drives a job to the terminal `canceled` state on request. Takes no
+ request body.
Cancel does not delete the job: the job record and any results already
produced are preserved for the normal retention window, the same as a
diff --git a/src/phenoml/lang2fhir_batch/types/batch_error.py b/src/phenoml/lang2fhir_batch/types/batch_error.py
index 29efa45..63e4ede 100644
--- a/src/phenoml/lang2fhir_batch/types/batch_error.py
+++ b/src/phenoml/lang2fhir_batch/types/batch_error.py
@@ -15,12 +15,12 @@ class BatchError(UniversalBaseModel):
"""
Short stable token to branch on. Item-level kinds: `invalid_input`
(the stored body was not a valid create/document request),
- `processing_failed` (the conversion failed), `budget_exceeded` (the
- item ran past its time budget — 600s for a document item, 450s for a
- create item), `result_too_large` (the result exceeded the storage
- cap), `input_unavailable` (the input could not be read), and
- `retries_exhausted` / `attempts_exhausted` (buried after too many
- failed attempts).
+ `processing_failed` (the conversion failed), `result_too_large` (the
+ result exceeded the storage cap), `input_unavailable` (the input
+ could not be read), and `retries_exhausted` / `attempts_exhausted`
+ (buried after too many interrupted attempts; an item has no
+ per-item time budget, but a single attempt is bounded by the
+ platform at 30 minutes, and an item cut off three times is buried).
Job-level kinds: `timeout` (the job did not finish within 36 hours
of creation).
"""
diff --git a/src/phenoml/lang2fhir_batch/types/batch_job.py b/src/phenoml/lang2fhir_batch/types/batch_job.py
index a574933..91bd46b 100644
--- a/src/phenoml/lang2fhir_batch/types/batch_job.py
+++ b/src/phenoml/lang2fhir_batch/types/batch_job.py
@@ -31,8 +31,8 @@ class BatchJob(UniversalBaseModel):
failure (the job could not run at all), distinct from individual item
failures, which never fail the job. `canceled` is a caller-requested
cancellation via `POST /lang2fhir/batch/{job_id}/cancel`; like the
- other terminal states it frees the job's active-job slot and keeps any
- results already produced readable for the retention window.
+ other terminal states it keeps any results already produced readable
+ for the retention window.
"""
finalized: bool = pydantic.Field()
diff --git a/src/phenoml/profiles/profiles/raw_client.py b/src/phenoml/profiles/profiles/raw_client.py
index 907ac1d..54ef7fc 100644
--- a/src/phenoml/profiles/profiles/raw_client.py
+++ b/src/phenoml/profiles/profiles/raw_client.py
@@ -527,6 +527,17 @@ def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] =
),
),
)
+ if _response.status_code == 409:
+ raise ConflictError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
if _response.status_code == 500:
raise InternalServerError(
headers=dict(_response.headers),
@@ -1051,6 +1062,17 @@ async def delete(
),
),
)
+ if _response.status_code == 409:
+ raise ConflictError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
if _response.status_code == 500:
raise InternalServerError(
headers=dict(_response.headers),
diff --git a/src/phenoml/profiles/versions/raw_client.py b/src/phenoml/profiles/versions/raw_client.py
index 0a1e0c1..7f77e6e 100644
--- a/src/phenoml/profiles/versions/raw_client.py
+++ b/src/phenoml/profiles/versions/raw_client.py
@@ -429,6 +429,17 @@ def delete(
),
),
)
+ if _response.status_code == 409:
+ raise ConflictError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
if _response.status_code == 500:
raise InternalServerError(
headers=dict(_response.headers),
@@ -853,6 +864,17 @@ async def delete(
),
),
)
+ if _response.status_code == 409:
+ raise ConflictError(
+ headers=dict(_response.headers),
+ body=typing.cast(
+ typing.Any,
+ parse_obj_as(
+ type_=typing.Any, # type: ignore
+ object_=_response.json(),
+ ),
+ ),
+ )
if _response.status_code == 500:
raise InternalServerError(
headers=dict(_response.headers),
diff --git a/tests/utils/test_http_client.py b/tests/utils/test_http_client.py
index 732c2f9..4cf2992 100644
--- a/tests/utils/test_http_client.py
+++ b/tests/utils/test_http_client.py
@@ -11,6 +11,7 @@
HttpClient,
_build_url,
_should_retry,
+ drop_content_type_without_body,
get_request_body,
remove_none_from_dict,
)
@@ -121,6 +122,67 @@ def test_explicit_empty_json_body_is_preserved() -> None:
assert data_body2 == {}
+def test_omitted_body_sends_no_content() -> None:
+ """A body left at the sentinel was never passed, so nothing is sent.
+
+ This is how an endpoint whose body may be omitted stays bodyless, as opposed to
+ sending an empty ``{}``. An explicit ``None`` still reaches the wire as ``null``.
+ """
+ omit = cast(Any, ...)
+ unrelated_request_options: RequestOptions = {"max_retries": 3}
+
+ json_body, data_body = get_request_body(json=omit, data=None, request_options=unrelated_request_options, omit=omit)
+ assert json_body is None
+ assert data_body is None
+
+ json_body2, data_body2 = get_request_body(
+ json=None, data=omit, request_options=unrelated_request_options, omit=omit
+ )
+ assert json_body2 is None
+ assert data_body2 is None
+
+ # an explicitly passed body is untouched
+ json_body3, _ = get_request_body(json={"hello": "world"}, data=None, request_options=None, omit=omit)
+ assert json_body3 == {"hello": "world"}
+
+
+def test_optional_body_sends_no_content_when_every_property_is_omitted() -> None:
+ """An endpoint that inlines an omittable body sends nothing once every property is omitted.
+
+ The body reaches the client as a dict of sentinels rather than as a single argument, so
+ ``optional_body`` is what tells the client that an empty result means "no body at all".
+ """
+ omit = cast(Any, ...)
+
+ json_body, data_body = get_request_body(
+ json={"amount": omit, "source": omit}, data=None, request_options=None, omit=omit, optional_body=True
+ )
+ assert json_body is None
+ assert data_body is None
+
+ # a body the API requires still goes out as `{}`
+ required_json_body, _ = get_request_body(
+ json={"amount": omit, "source": omit}, data=None, request_options=None, omit=omit
+ )
+ assert required_json_body == {}
+
+ # a property the caller did pass keeps the body
+ populated_json_body, _ = get_request_body(
+ json={"amount": 1, "source": omit}, data=None, request_options=None, omit=omit, optional_body=True
+ )
+ assert populated_json_body == {"amount": 1}
+
+ # additional body parameters keep the body, since the caller asked for them
+ with_additional_body_parameters, _ = get_request_body(
+ json={"amount": omit},
+ data=None,
+ request_options={"additional_body_parameters": {"custom": "value"}},
+ omit=omit,
+ optional_body=True,
+ )
+ assert with_additional_body_parameters == {"custom": "value"}
+
+
def test_json_body_preserves_none_values() -> None:
"""Test that JSON bodies preserve None values (they become JSON null)."""
json_body, data_body = get_request_body(
@@ -672,28 +734,20 @@ def _make_response(status_code: int) -> httpx.Response:
return httpx.Response(status_code=status_code, content=b"")
-@pytest.mark.parametrize(
- "status_code",
- [408, 409, 429, 500, 501, 502, 503, 504, 599],
-)
+RETRYABLE_STATUS_CODES = [408, 409, 429, 500, 501, 502, 503, 504, 599]
+NON_RETRYABLE_STATUS_CODES = [200, 201, 301, 400, 401, 403, 404]
+
+
+@pytest.mark.parametrize("status_code", RETRYABLE_STATUS_CODES)
def test_should_retry_retryable_status_codes(status_code: int) -> None:
- """Legacy mode: retries on 408, 409, 429, and all >= 500."""
assert _should_retry(_make_response(status_code)) is True
-@pytest.mark.parametrize(
- "status_code",
- [200, 201, 301, 400, 401, 403, 404],
-)
+@pytest.mark.parametrize("status_code", NON_RETRYABLE_STATUS_CODES)
def test_should_not_retry_non_retryable_status_codes(status_code: int) -> None:
assert _should_retry(_make_response(status_code)) is False
-def test_should_retry_599_upper_boundary() -> None:
- """Legacy mode retries on >= 500, which includes 599."""
- assert _should_retry(_make_response(599)) is True
-
-
# ---------------------------------------------------------------------------
# RequestOptions timeout resolution tests (timeout / deprecated timeout_in_seconds)
# ---------------------------------------------------------------------------
@@ -759,3 +813,33 @@ async def test_async_request_options_timeout_takes_precedence() -> None:
)
await http_client.request(path="/test", method="GET", request_options={"timeout": 30, "timeout_in_seconds": 45})
assert dummy_client.last_request_kwargs["timeout"] == 30
+
+
+def test_drop_content_type_without_body_omits_header_for_bodyless_optional_call() -> None:
+ """An optional-body endpoint the caller left empty must not advertise a media type.
+
+ `get_request_body` drops the body, but the endpoint still hands over the content type it
+ would have used, so a server that branches on the header would see a JSON request carrying
+ nothing at all.
+ """
+ headers = {"content-type": "application/json", "authorization": "Bearer x"}
+
+ assert drop_content_type_without_body(headers, json_body=None, data_body=None, optional_body=True) == {
+ "authorization": "Bearer x"
+ }
+
+
+def test_drop_content_type_without_body_keeps_header_when_a_body_is_sent() -> None:
+ headers = {"content-type": "application/json"}
+
+ assert (
+ drop_content_type_without_body(headers, json_body={"amount": 60}, data_body=None, optional_body=True) == headers
+ )
+ assert drop_content_type_without_body(headers, json_body=None, data_body="raw", optional_body=True) == headers
+
+
+def test_drop_content_type_without_body_leaves_required_body_endpoints_alone() -> None:
+ """Without the opt-in, an endpoint keeps the header it has always sent."""
+ headers = {"Content-Type": "application/json"}
+
+ assert drop_content_type_without_body(headers, json_body=None, data_body=None, optional_body=False) == headers
diff --git a/tests/wire/test_implementationGuides_implementationGuides.py b/tests/wire/test_implementationGuides_implementationGuides.py
index f65a229..f0cef92 100644
--- a/tests/wire/test_implementationGuides_implementationGuides.py
+++ b/tests/wire/test_implementationGuides_implementationGuides.py
@@ -1,5 +1,7 @@
from .conftest import get_client, verify_request_count
+from phenoml.implementation_guides import FhirImplementationGuide
+
def test_implementationGuides_implementationGuides_list_() -> None:
"""Test list endpoint with WireMock"""
@@ -37,3 +39,30 @@ def test_implementationGuides_implementationGuides_delete() -> None:
name="acme-cardiology",
)
verify_request_count(test_id, "DELETE", "/fhir/implementation-guides/acme-cardiology", None, 1)
+
+
+def test_implementationGuides_implementationGuides_create_version() -> None:
+ """Test createVersion endpoint with WireMock"""
+ test_id = "implementation_guides.implementation_guides.create_version.0"
+ client = get_client(test_id)
+ client.implementation_guides.implementation_guides.create_version(
+ name="name",
+ implementation_guide=FhirImplementationGuide(
+ resource_type="ImplementationGuide",
+ url="url",
+ version="version",
+ ),
+ profile_refs=["profile_refs"],
+ )
+ verify_request_count(test_id, "POST", "/fhir/implementation-guides/name/versions", None, 1)
+
+
+def test_implementationGuides_implementationGuides_get_version() -> None:
+ """Test getVersion endpoint with WireMock"""
+ test_id = "implementation_guides.implementation_guides.get_version.0"
+ client = get_client(test_id)
+ client.implementation_guides.implementation_guides.get_version(
+ name="name",
+ version="1.0.0",
+ )
+ verify_request_count(test_id, "GET", "/fhir/implementation-guides/name/versions/1.0.0", None, 1)
diff --git a/wiremock/wiremock-mappings.json b/wiremock/wiremock-mappings.json
index 9d3fc77..94e1e06 100644
--- a/wiremock/wiremock-mappings.json
+++ b/wiremock/wiremock-mappings.json
@@ -5,7 +5,12 @@
"name": "Create a new agent - default",
"request": {
"urlPathTemplate": "/agent/create",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 201,
@@ -32,6 +37,11 @@
"request": {
"urlPathTemplate": "/agent/list",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"queryParameters": {
"tags": {
"equalTo": "tags"
@@ -64,6 +74,11 @@
"request": {
"urlPathTemplate": "/agent/{id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -95,6 +110,11 @@
"request": {
"urlPathTemplate": "/agent/{id}",
"method": "PUT",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -126,6 +146,11 @@
"request": {
"urlPathTemplate": "/agent/{id}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -157,6 +182,11 @@
"request": {
"urlPathTemplate": "/agent/{id}",
"method": "PATCH",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -187,7 +217,12 @@
"name": "Chat with agent - Query Patient Condition",
"request": {
"urlPathTemplate": "/agent/chat",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -213,7 +248,12 @@
"name": "Chat with agent (streaming) - default",
"request": {
"urlPathTemplate": "/agent/stream-chat",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -240,6 +280,11 @@
"request": {
"urlPathTemplate": "/agent/chat/messages",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"queryParameters": {
"chat_session_id": {
"equalTo": "chat_session_id"
@@ -280,7 +325,12 @@
"name": "Create agent prompt - default",
"request": {
"urlPathTemplate": "/agent/prompts",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 201,
@@ -306,7 +356,12 @@
"name": "List agent prompts - default",
"request": {
"urlPathTemplate": "/agent/prompts/list",
- "method": "GET"
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -334,6 +389,11 @@
"request": {
"urlPathTemplate": "/agent/prompts/{id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -365,6 +425,11 @@
"request": {
"urlPathTemplate": "/agent/prompts/{id}",
"method": "PUT",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -396,6 +461,11 @@
"request": {
"urlPathTemplate": "/agent/prompts/{id}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -427,6 +497,11 @@
"request": {
"urlPathTemplate": "/agent/prompts/{id}",
"method": "PATCH",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -483,7 +558,12 @@
"name": "Analyze text for patient cohort criteria - default",
"request": {
"urlPathTemplate": "/cohort",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -509,7 +589,12 @@
"name": "Upload custom code system - JSON codes (preferred)",
"request": {
"urlPathTemplate": "/construe/upload",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 202,
@@ -535,7 +620,12 @@
"name": "List available code systems - default",
"request": {
"urlPathTemplate": "/construe/codes/systems",
- "method": "GET"
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -563,6 +653,11 @@
"request": {
"urlPathTemplate": "/construe/codes/systems/{codesystem}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"codesystem": {
"equalTo": "ICD-10-CM"
@@ -599,6 +694,11 @@
"request": {
"urlPathTemplate": "/construe/codes/systems/{codesystem}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"codesystem": {
"equalTo": "CUSTOM_CODES"
@@ -635,6 +735,11 @@
"request": {
"urlPathTemplate": "/construe/codes/systems/{codesystem}/export",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"codesystem": {
"equalTo": "CUSTOM_CODES"
@@ -670,7 +775,12 @@
"name": "Submit feedback on extraction results - default",
"request": {
"urlPathTemplate": "/construe/feedback",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 201,
@@ -696,7 +806,12 @@
"name": "Extract medical codes from text - Basic Extraction",
"request": {
"urlPathTemplate": "/construe/extract",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -722,7 +837,12 @@
"name": "[Alpha] Extract medical codes with phenocr - HPO Phenotype Extraction",
"request": {
"urlPathTemplate": "/construe/phenocr",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -748,7 +868,12 @@
"name": "Crosswalk a code to target code systems - ICD-10-CM to HPO",
"request": {
"urlPathTemplate": "/construe/codes/crosswalk",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -775,6 +900,11 @@
"request": {
"urlPathTemplate": "/construe/codes/{codesystem}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"codesystem": {
"equalTo": "ICD-10-CM"
@@ -817,6 +947,11 @@
"request": {
"urlPathTemplate": "/construe/codes/{codesystem}/{codeID}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"codesystem": {
"equalTo": "ICD-10-CM"
@@ -856,6 +991,11 @@
"request": {
"urlPathTemplate": "/construe/codes/{codesystem}/search/semantic",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"codesystem": {
"equalTo": "ICD-10-CM"
@@ -898,6 +1038,11 @@
"request": {
"urlPathTemplate": "/construe/codes/{codesystem}/search/text",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"codesystem": {
"equalTo": "ICD-10-CM"
@@ -940,6 +1085,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/fhir/{fhir_path}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "550e8400-e29b-41d4-a716-446655440000"
@@ -974,6 +1124,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/fhir/{fhir_path}",
"method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "550e8400-e29b-41d4-a716-446655440000"
@@ -1008,6 +1163,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/fhir/{fhir_path}",
"method": "PUT",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "550e8400-e29b-41d4-a716-446655440000"
@@ -1042,6 +1202,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/fhir/{fhir_path}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "550e8400-e29b-41d4-a716-446655440000"
@@ -1076,6 +1241,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/fhir/{fhir_path}",
"method": "PATCH",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "550e8400-e29b-41d4-a716-446655440000"
@@ -1110,6 +1280,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/fhir",
"method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "550e8400-e29b-41d4-a716-446655440000"
@@ -1140,7 +1315,12 @@
"name": "Map FHIR resources to OMOP CDM v5.4 - Mapping result",
"request": {
"urlPathTemplate": "/fhir2omop/create",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -1166,7 +1346,12 @@
"name": "Create FHIR provider - default",
"request": {
"urlPathTemplate": "/fhir-provider",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 201,
@@ -1192,7 +1377,12 @@
"name": "List FHIR providers - default",
"request": {
"urlPathTemplate": "/fhir-provider/list",
- "method": "GET"
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -1220,6 +1410,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "fhir_provider_id"
@@ -1251,6 +1446,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "fhir_provider_id"
@@ -1282,6 +1482,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/add-auth-config",
"method": "PATCH",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "1716d214-de93-43a4-aa6b-a878d864e2ad"
@@ -1313,6 +1518,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/set-active-auth-config",
"method": "PATCH",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "1716d214-de93-43a4-aa6b-a878d864e2ad"
@@ -1344,6 +1554,11 @@
"request": {
"urlPathTemplate": "/fhir-provider/{fhir_provider_id}/remove-auth-config",
"method": "PATCH",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"fhir_provider_id": {
"equalTo": "1716d214-de93-43a4-aa6b-a878d864e2ad"
@@ -1374,11 +1589,16 @@
"name": "List implementation guides - default",
"request": {
"urlPathTemplate": "/fhir/implementation-guides",
- "method": "GET"
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
- "body": "{\n \"implementation_guides\": [\n {\n \"name\": \"acme-cardiology\",\n \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n \"profile_count\": 3,\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n }\n ]\n}",
+ "body": "{\n \"implementation_guides\": [\n {\n \"name\": \"acme-cardiology\",\n \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n \"profile_count\": 3,\n \"canonical_url\": \"canonical_url\",\n \"version_count\": 1,\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n }\n ]\n}",
"headers": {
"Content-Type": "application/json"
}
@@ -1402,6 +1622,11 @@
"request": {
"urlPathTemplate": "/fhir/implementation-guides/{name}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"name": {
"equalTo": "acme-cardiology"
@@ -1410,7 +1635,7 @@
},
"response": {
"status": 200,
- "body": "{\n \"name\": \"acme-cardiology\",\n \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n \"profile_count\": 3,\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\",\n \"profiles\": [\n \"custom-patient\",\n \"acme-vital-signs\"\n ]\n}",
+ "body": "{\n \"name\": \"acme-cardiology\",\n \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n \"profile_count\": 3,\n \"canonical_url\": \"canonical_url\",\n \"version_count\": 1,\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\",\n \"profiles\": [\n \"custom-patient\",\n \"acme-vital-signs\"\n ]\n}",
"headers": {
"Content-Type": "application/json"
}
@@ -1433,6 +1658,11 @@
"request": {
"urlPathTemplate": "/fhir/implementation-guides/{name}",
"method": "PUT",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"name": {
"equalTo": "acme-cardiology"
@@ -1441,7 +1671,7 @@
},
"response": {
"status": 200,
- "body": "{\n \"name\": \"acme-cardiology\",\n \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n \"profile_count\": 3,\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n}",
+ "body": "{\n \"name\": \"acme-cardiology\",\n \"profile_context\": \"When the text mentions phenotypic features, prefer the hpo-observation profile over Condition.\",\n \"profile_count\": 3,\n \"canonical_url\": \"canonical_url\",\n \"version_count\": 1,\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n}",
"headers": {
"Content-Type": "application/json"
}
@@ -1459,11 +1689,16 @@
}
},
{
- "id": "44652922-4793-4275-840e-4dc1b1bde786",
- "name": "Delete an implementation guide's metadata - default",
+ "id": "2678a6e7-10b0-4621-ad5c-67980574a8f8",
+ "name": "Delete an implementation guide family - default",
"request": {
"urlPathTemplate": "/fhir/implementation-guides/{name}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"name": {
"equalTo": "acme-cardiology"
@@ -1477,7 +1712,82 @@
"Content-Type": "application/json"
}
},
- "uuid": "44652922-4793-4275-840e-4dc1b1bde786",
+ "uuid": "2678a6e7-10b0-4621-ad5c-67980574a8f8",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ }
+ },
+ {
+ "id": "a7a30315-de87-4485-a1e4-15eeea48b530",
+ "name": "Publish an exact custom-profile package - default",
+ "request": {
+ "urlPathTemplate": "/fhir/implementation-guides/{name}/versions",
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
+ "pathParameters": {
+ "name": {
+ "equalTo": "name"
+ }
+ }
+ },
+ "response": {
+ "status": 201,
+ "body": "{\n \"name\": \"name\",\n \"url\": \"url\",\n \"version\": \"version\",\n \"profile_context\": \"profile_context\",\n \"profiles\": [\n \"profiles\"\n ],\n \"profile_refs\": [\n \"profile_refs\"\n ],\n \"implementation_guide\": {\n \"resourceType\": \"ImplementationGuide\",\n \"id\": \"id\",\n \"url\": \"url\",\n \"version\": \"version\",\n \"name\": \"name\",\n \"status\": \"status\",\n \"packageId\": \"packageId\",\n \"fhirVersion\": [\n \"fhirVersion\"\n ]\n },\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "a7a30315-de87-4485-a1e4-15eeea48b530",
+ "persistent": true,
+ "priority": 3,
+ "metadata": {
+ "mocklab": {
+ "created": {
+ "at": "2020-01-01T00:00:00.000Z",
+ "via": "SYSTEM"
+ }
+ }
+ }
+ },
+ {
+ "id": "bfd69c60-e799-438e-a780-2c4c1fd1a9a8",
+ "name": "Get an exact custom-profile package - default",
+ "request": {
+ "urlPathTemplate": "/fhir/implementation-guides/{name}/versions/{version}",
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
+ "pathParameters": {
+ "name": {
+ "equalTo": "name"
+ },
+ "version": {
+ "equalTo": "1.0.0"
+ }
+ }
+ },
+ "response": {
+ "status": 200,
+ "body": "{\n \"name\": \"name\",\n \"url\": \"url\",\n \"version\": \"version\",\n \"profile_context\": \"profile_context\",\n \"profiles\": [\n \"profiles\"\n ],\n \"profile_refs\": [\n \"profile_refs\"\n ],\n \"implementation_guide\": {\n \"resourceType\": \"ImplementationGuide\",\n \"id\": \"id\",\n \"url\": \"url\",\n \"version\": \"version\",\n \"name\": \"name\",\n \"status\": \"status\",\n \"packageId\": \"packageId\",\n \"fhirVersion\": [\n \"fhirVersion\"\n ]\n },\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n}",
+ "headers": {
+ "Content-Type": "application/json"
+ }
+ },
+ "uuid": "bfd69c60-e799-438e-a780-2c4c1fd1a9a8",
"persistent": true,
"priority": 3,
"metadata": {
@@ -1494,7 +1804,12 @@
"name": "Create FHIR resource from text - Condition Resource",
"request": {
"urlPathTemplate": "/lang2fhir/create",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -1520,7 +1835,12 @@
"name": "Extract multiple FHIR resources from text - Full Patient Record",
"request": {
"urlPathTemplate": "/lang2fhir/create/multi",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -1546,7 +1866,12 @@
"name": "Generate FHIR search parameters from text - Search Appointments",
"request": {
"urlPathTemplate": "/lang2fhir/search",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -1572,7 +1897,12 @@
"name": "Upload custom FHIR profile (deprecated) - default",
"request": {
"urlPathTemplate": "/lang2fhir/profile/upload",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 201,
@@ -1598,7 +1928,12 @@
"name": "Convert document to FHIR resource - default",
"request": {
"urlPathTemplate": "/lang2fhir/document",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -1624,7 +1959,12 @@
"name": "Extract multiple FHIR resources from a document - default",
"request": {
"urlPathTemplate": "/lang2fhir/document/multi",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -1651,6 +1991,11 @@
"request": {
"urlPathTemplate": "/lang2fhir/batch",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"queryParameters": {
"cursor": {
"equalTo": "cursor"
@@ -1685,7 +2030,12 @@
"name": "Create a batch job - default",
"request": {
"urlPathTemplate": "/lang2fhir/batch",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 202,
@@ -1712,6 +2062,11 @@
"request": {
"urlPathTemplate": "/lang2fhir/batch/{job_id}/items",
"method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"job_id": {
"equalTo": "job_id"
@@ -1743,6 +2098,11 @@
"request": {
"urlPathTemplate": "/lang2fhir/batch/{job_id}/finalize",
"method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"job_id": {
"equalTo": "job_id"
@@ -1774,6 +2134,11 @@
"request": {
"urlPathTemplate": "/lang2fhir/batch/{job_id}/cancel",
"method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"job_id": {
"equalTo": "job_id"
@@ -1805,6 +2170,11 @@
"request": {
"urlPathTemplate": "/lang2fhir/batch/{job_id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"job_id": {
"equalTo": "job_id"
@@ -1844,6 +2214,11 @@
"request": {
"urlPathTemplate": "/lang2fhir/batch/{job_id}/results",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"job_id": {
"equalTo": "job_id"
@@ -1883,6 +2258,11 @@
"request": {
"urlPathTemplate": "/lang2fhir/batch/{job_id}/results/{item_id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"job_id": {
"equalTo": "job_id"
@@ -1917,6 +2297,11 @@
"request": {
"urlPathTemplate": "/fhir/profiles",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"queryParameters": {
"url": {
"equalTo": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0"
@@ -1948,7 +2333,12 @@
"name": "Upload a custom FHIR profile - default",
"request": {
"urlPathTemplate": "/fhir/profiles",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 201,
@@ -1975,6 +2365,11 @@
"request": {
"urlPathTemplate": "/fhir/profiles/{id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "custom-patient"
@@ -2006,6 +2401,11 @@
"request": {
"urlPathTemplate": "/fhir/profiles/{id}",
"method": "PUT",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "custom-patient"
@@ -2037,6 +2437,11 @@
"request": {
"urlPathTemplate": "/fhir/profiles/{id}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "custom-patient"
@@ -2068,6 +2473,11 @@
"request": {
"urlPathTemplate": "/fhir/profiles/{id}/versions",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "custom-patient"
@@ -2099,6 +2509,11 @@
"request": {
"urlPathTemplate": "/fhir/profiles/{id}/versions",
"method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "custom-patient"
@@ -2130,6 +2545,11 @@
"request": {
"urlPathTemplate": "/fhir/profiles/{id}/versions/{version}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "custom-patient"
@@ -2164,6 +2584,11 @@
"request": {
"urlPathTemplate": "/fhir/profiles/{id}/versions/{version}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "custom-patient"
@@ -2197,7 +2622,12 @@
"name": "List all summary templates - default",
"request": {
"urlPathTemplate": "/fhir2summary/templates",
- "method": "GET"
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2224,7 +2654,12 @@
"name": "Create a new summary template - Discharge Summary Template",
"request": {
"urlPathTemplate": "/fhir2summary/template",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2251,6 +2686,11 @@
"request": {
"urlPathTemplate": "/fhir2summary/template/{id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -2282,6 +2722,11 @@
"request": {
"urlPathTemplate": "/fhir2summary/template/{id}",
"method": "PUT",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -2313,6 +2758,11 @@
"request": {
"urlPathTemplate": "/fhir2summary/template/{id}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -2343,7 +2793,12 @@
"name": "Generate a summary from FHIR resources - Narrative Summary",
"request": {
"urlPathTemplate": "/fhir2summary/create",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2369,7 +2824,12 @@
"name": "Create FHIR resource from text and store it - default",
"request": {
"urlPathTemplate": "/tools/lang2fhir-and-create",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2395,7 +2855,12 @@
"name": "Extract and store multiple FHIR resources - default",
"request": {
"urlPathTemplate": "/tools/lang2fhir-and-create-multi",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2421,7 +2886,12 @@
"name": "Search FHIR resources from natural language - default",
"request": {
"urlPathTemplate": "/tools/lang2fhir-and-search",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2447,7 +2917,12 @@
"name": "Analyze patient cohorts - default",
"request": {
"urlPathTemplate": "/tools/cohort",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2473,7 +2948,12 @@
"name": "Create MCP server - default",
"request": {
"urlPathTemplate": "/tools/mcp-server/create",
- "method": "POST"
+ "method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2499,7 +2979,12 @@
"name": "List MCP servers - default",
"request": {
"urlPathTemplate": "/tools/mcp-server/list",
- "method": "GET"
+ "method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ }
},
"response": {
"status": 200,
@@ -2527,6 +3012,11 @@
"request": {
"urlPathTemplate": "/tools/mcp-server/{mcp_server_id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"mcp_server_id": {
"equalTo": "mcp_server_id"
@@ -2558,6 +3048,11 @@
"request": {
"urlPathTemplate": "/tools/mcp-server/{mcp_server_id}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"mcp_server_id": {
"equalTo": "mcp_server_id"
@@ -2589,6 +3084,11 @@
"request": {
"urlPathTemplate": "/tools/mcp-server/{mcp_server_id}/list",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"mcp_server_id": {
"equalTo": "mcp_server_id"
@@ -2620,6 +3120,11 @@
"request": {
"urlPathTemplate": "/tools/mcp-server/tool/{mcp_server_tool_id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"mcp_server_tool_id": {
"equalTo": "mcp_server_tool_id"
@@ -2651,6 +3156,11 @@
"request": {
"urlPathTemplate": "/tools/mcp-server/tool/{mcp_server_tool_id}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"mcp_server_tool_id": {
"equalTo": "mcp_server_tool_id"
@@ -2682,6 +3192,11 @@
"request": {
"urlPathTemplate": "/workflows",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"queryParameters": {
"verbose": {
"equalTo": "true"
@@ -2714,6 +3229,11 @@
"request": {
"urlPathTemplate": "/workflows",
"method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"queryParameters": {
"verbose": {
"equalTo": "true"
@@ -2745,6 +3265,11 @@
"request": {
"urlPathTemplate": "/workflows/{id}",
"method": "GET",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -2781,6 +3306,11 @@
"request": {
"urlPathTemplate": "/workflows/{id}",
"method": "PUT",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -2817,6 +3347,11 @@
"request": {
"urlPathTemplate": "/workflows/{id}",
"method": "DELETE",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "id"
@@ -2848,6 +3383,11 @@
"request": {
"urlPathTemplate": "/workflows/{id}/execute",
"method": "POST",
+ "headers": {
+ "Authorization": {
+ "matches": "Bearer .+"
+ }
+ },
"pathParameters": {
"id": {
"equalTo": "7a8b9c0d-1234-5678-abcd-ef9876543210"
@@ -2875,6 +3415,6 @@
}
],
"meta": {
- "total": 94
+ "total": 96
}
}
\ No newline at end of file
From 3822b304a9e903df731a21f72cb00f9c68976ead Mon Sep 17 00:00:00 2001
From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 15:53:50 +0000
Subject: [PATCH 2/4] [fern-autoversion] feat: add implementation guide
versioning and TCP keepalive support
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Introduce `create_version` and `get_version` methods on the
`ImplementationGuidesClient` (sync and async) for publishing and
retrieving exact canonical FHIR package versions beneath a guide family.
Also adds TCP keepalive socket options to the HTTP transport layer and
improves optional-body handling to suppress `Content-Type` headers on
bodyless requests.
Key changes:
- Add `create_version(name, *, implementation_guide, profile_refs, profile_context)` and `get_version(name, version)` to sync and async `ImplementationGuidesClient`, returning `ImplementationGuideVersionDetail`
- Add `get_keepalive_socket_options()` helper and wire TCP keepalive into `HttpClient` / `AsyncHttpClient` transports to keep idle connections alive through firewalls and NAT
- Add `optional_body` parameter to `get_request_body` and HTTP client request methods; add `drop_content_type_without_body` to strip `Content-Type` when no body is sent
- Refactor `_coerce_field_names_to_aliases` into a shared `_coerce_keys_to_aliases` helper with a `WeakKeyDictionary` cache to avoid redundant field introspection per model class
🌿 Generated with Fern
---
.fern/metadata.json | 2 +-
changelog.md | 17 +++++++++++++++++
pyproject.toml | 2 +-
src/phenoml/core/client_wrapper.py | 2 +-
4 files changed, 20 insertions(+), 3 deletions(-)
diff --git a/.fern/metadata.json b/.fern/metadata.json
index a13b8b6..5f5d026 100644
--- a/.fern/metadata.json
+++ b/.fern/metadata.json
@@ -13,5 +13,5 @@
"invokedBy": "ci",
"requestedVersion": "AUTO",
"ciProvider": "unknown",
- "sdkVersion": "0.0.0.dev0"
+ "sdkVersion": "17.1.0"
}
\ No newline at end of file
diff --git a/changelog.md b/changelog.md
index 2a9ecbb..dfe0407 100644
--- a/changelog.md
+++ b/changelog.md
@@ -1,3 +1,20 @@
+## [17.1.0] - 2026-09-14
+### Added
+- **`client.lang2fhir_batch`** — new sync and async client for asynchronous batch FHIR extraction supporting the full job lifecycle (`create`, `upload_item`, `finalize`, `cancel`, `get`, `get_results`, `get_result`, `list`), along with supporting Pydantic models (`BatchJob`, `BatchError`, `BatchCounts`, `BatchItemStatus`, `UploadItemResponse`, `JobDetailResponse`, `ResultsPageResponse`, `JobListResponse`).
+- **`client.implementation_guides.create_version(...)` and `get_version(...)`** — new sync and async methods to publish and retrieve immutable exact canonical FHIR package versions beneath a guide family; `ConflictError` (HTTP 409) is raised on duplicate versions; new models `FhirImplementationGuide` and `ImplementationGuideVersionDetail` are exported from `phenoml.implementation_guides`.
+- **`client.profiles.versions`** — new sync and async client for immutable retained StructureDefinition versions with `list`, `create`, `get`, and `delete` methods, plus `ProfileVersionCreateRequest` and `ProfileVersionListResponse` models.
+- **`profiles_update` and `profiles_delete`** — new `PUT` and `DELETE` operations on the profiles client for in-place profile replacement and permanent deletion.
+- **New optional fields** — `ImplementationGuideSummary.canonical_url` and `ImplementationGuideSummary.version_count`; `ProfileSummary.status`, `ProfileSummary.date`, and `ProfileSummary.canonical` added to their respective summary models.
+
+### Changed
+- **`token` parameter on `PhenomlClient` and `AsyncPhenomlClient`** — now accepts `Union[str, Callable[[], str]]`, allowing a plain bearer token string to be passed directly without wrapping it in a callable.
+- **`lang2fhir_batch` active-job limit** — the previous 4-active-job cap has been removed; HTTP 409 is no longer raised by `create`, and item-level retry errors now reflect a 30-minute per-attempt platform bound.
+- **`OAuthTokenProvider`** — `grant_type="client_credentials"` is now explicitly sent on every token refresh (sync and async).
+- **HTTP transport** — TCP keepalive is now applied to all connections, and requests with no body no longer send a spurious `Content-Type` header.
+
+### Fixed
+- **Agent chat SSE stream** — empty SSE data frames are now skipped instead of raising a parse error in sync and async streaming responses.
+
## [17.0.0] - 2026-09-09
### Breaking Changes
- **`ProfileSummary`** — `id`, `source`, `resource_type`, `url`, `version`, `fhir_version`, `implementation_guide`, `created_at`, and `updated_at` are now required; remove `None` guards for these fields.
diff --git a/pyproject.toml b/pyproject.toml
index 1dc545d..ccf246c 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -4,7 +4,7 @@ dynamic = ["version"]
[tool.poetry]
name = "phenoml"
-version = "0.0.0.dev0"
+version = "17.1.0"
description = ""
readme = "README.md"
authors = []
diff --git a/src/phenoml/core/client_wrapper.py b/src/phenoml/core/client_wrapper.py
index c03a243..9e27324 100644
--- a/src/phenoml/core/client_wrapper.py
+++ b/src/phenoml/core/client_wrapper.py
@@ -38,7 +38,7 @@ def get_headers(self) -> typing.Dict[str, str]:
"X-Fern-Runtime": f"python/{platform.python_version()}",
"X-Fern-Platform": f"{platform.system().lower()}/{platform.release()}",
"X-Fern-SDK-Name": "phenoml",
- "X-Fern-SDK-Version": "0.0.0.dev0",
+ "X-Fern-SDK-Version": "17.1.0",
**(self.get_custom_headers() or {}),
}
token = self._get_token()
From 5d42f72a00ce267ee7711ff529fe5688a604c202 Mon Sep 17 00:00:00 2001
From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 15:53:50 +0000
Subject: [PATCH 3/4] [fern-replay] Applied customizations
Patches applied (1):
- patch-6516695e: Release 15.0.2: restore bundled openapi.json packaging (#169)
---
.fern/replay.lock | 27 +++++++++++++++++++++------
pyproject.toml | 3 +++
2 files changed, 24 insertions(+), 6 deletions(-)
diff --git a/.fern/replay.lock b/.fern/replay.lock
index 25dc6cb..ed15218 100644
--- a/.fern/replay.lock
+++ b/.fern/replay.lock
@@ -120,21 +120,36 @@ generations:
cli_version: unknown
generator_versions:
fernapi/fern-python-sdk: 5.18.1
-current_generation: 3ac2feef8a9595d7dea2a99b024322ae61959133
+ - commit_sha: 618aa073944e4bca430657c9e239d6827157df79
+ tree_hash: c0526d155b3fb81c65d2d77286733db11f1f57b7
+ timestamp: 2026-09-14T15:53:01.388Z
+ cli_version: unknown
+ generator_versions:
+ fernapi/fern-python-sdk: 5.30.0
+current_generation: 618aa073944e4bca430657c9e239d6827157df79
patches:
- id: patch-6516695e
- content_hash: sha256:5caeff601ccd2db4bda36068ff6c1ac7dba46f1c049cccc367a7db553613acad
+ content_hash: sha256:4b07085503a73b0d7551a0483d04ec6c821df4b63f77762193d83acc0307f6db
original_commit: 6516695ecaba47ae4bcc8119acca86a1113adeeb
original_message: "Release 15.0.2: restore bundled openapi.json packaging (#169)"
original_author: Gavin Sharp
- base_generation: 3ac2feef8a9595d7dea2a99b024322ae61959133
+ base_generation: 618aa073944e4bca430657c9e239d6827157df79
files:
- pyproject.toml
patch_content: |
diff --git a/pyproject.toml b/pyproject.toml
- index cea8b20..f3f1f73 100644
+ index 1dc545d..4b6ff5e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
+ @@ -4,7 +4,7 @@ dynamic = ["version"]
+
+ [tool.poetry]
+ name = "phenoml"
+ -version = "0.0.0.dev0"
+ +version = "17.1.0"
+ description = ""
+ readme = "README.md"
+ authors = []
@@ -31,6 +31,9 @@ classifiers = [
packages = [
{ include = "phenoml", from = "src"}
@@ -153,7 +168,7 @@ patches:
[tool.poetry]
name = "phenoml"
- version = "17.0.0"
+ version = "17.1.0"
description = ""
readme = "README.md"
authors = []
@@ -191,7 +206,7 @@ patches:
python = "^3.10"
aiohttp = { version = ">=3.14.1,<4", optional = true, python = ">=3.10"}
httpx = ">=0.21.2"
- httpx-aiohttp = { version = "0.1.8", optional = true, python = ">=3.10"}
+ httpx-aiohttp = { version = "^0.1.8", optional = true, python = ">=3.10"}
pydantic = ">= 1.9.2"
pydantic-core = ">=2.18.2,<3.0.0"
typing_extensions = ">= 4.0.0"
diff --git a/pyproject.toml b/pyproject.toml
index ccf246c..4b6ff5e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -31,6 +31,9 @@ classifiers = [
packages = [
{ include = "phenoml", from = "src"}
]
+include = [
+ { path = "src/phenoml/openapi/openapi.json", format = ["sdist", "wheel"] }
+]
[tool.poetry.urls]
Repository = 'https://github.com/phenoml/phenoml-python-sdk'
From 664c88374d44c6ebe3dbf6c914514f7bdef5e2df Mon Sep 17 00:00:00 2001
From: "github-actions[bot]"
<41898282+github-actions[bot]@users.noreply.github.com>
Date: Mon, 14 Sep 2026 15:54:04 +0000
Subject: [PATCH 4/4] chore: sync OpenAPI spec + code-examples for
e555f47326197aad510a783942045e7c53476fc5 [skip ci]
---
code-examples.json | 75 ++++++++-
src/phenoml/openapi/openapi.json | 267 +++++++++++++++++++++++++++++--
2 files changed, 327 insertions(+), 15 deletions(-)
diff --git a/code-examples.json b/code-examples.json
index 79472cc..92406ca 100644
--- a/code-examples.json
+++ b/code-examples.json
@@ -2,8 +2,8 @@
"metadata": {
"language": "python",
"packageName": "phenoml",
- "sdkVersion": "17.0.0",
- "specCommit": "26224ef37e22adb507f28c21e8991d4a27822bde",
+ "sdkVersion": "17.1.0",
+ "specCommit": "e555f47326197aad510a783942045e7c53476fc5",
"generatorName": "fernapi/fern-python-sdk"
},
"renderRules": {
@@ -3957,6 +3957,77 @@
]
}
},
+ "POST /fhir/implementation-guides/{name}/versions": {
+ "httpMethod": "POST",
+ "httpPath": "/fhir/implementation-guides/{name}/versions",
+ "request": {
+ "body": null
+ },
+ "response": {
+ "body": null
+ },
+ "render": {
+ "callTemplate": "client.implementation_guides.implementation_guides.create_version(name={{name}}, {{__body__}})",
+ "params": [
+ {
+ "name": "name",
+ "kind": "string"
+ }
+ ],
+ "body": {
+ "fieldSeparator": ", ",
+ "fields": [
+ {
+ "jsonKey": "implementation_guide",
+ "fieldTemplate": "implementation_guide={{value}}",
+ "kind": "object",
+ "required": true
+ },
+ {
+ "jsonKey": "profile_refs",
+ "fieldTemplate": "profile_refs={{value}}",
+ "kind": "list",
+ "required": true,
+ "items": {
+ "jsonKey": "",
+ "fieldTemplate": "{{value}}",
+ "kind": "string",
+ "required": true
+ }
+ },
+ {
+ "jsonKey": "profile_context",
+ "fieldTemplate": "profile_context={{value}}",
+ "kind": "string",
+ "required": false
+ }
+ ]
+ }
+ }
+ },
+ "GET /fhir/implementation-guides/{name}/versions/{version}": {
+ "httpMethod": "GET",
+ "httpPath": "/fhir/implementation-guides/{name}/versions/{version}",
+ "request": {
+ "body": null
+ },
+ "response": {
+ "body": null
+ },
+ "render": {
+ "callTemplate": "client.implementation_guides.implementation_guides.get_version(name={{name}}, version={{version}})",
+ "params": [
+ {
+ "name": "name",
+ "kind": "string"
+ },
+ {
+ "name": "version",
+ "kind": "string"
+ }
+ ]
+ }
+ },
"GET /fhir/profiles": {
"httpMethod": "GET",
"httpPath": "/fhir/profiles",
diff --git a/src/phenoml/openapi/openapi.json b/src/phenoml/openapi/openapi.json
index 0a3b0ed..0c1f41c 100644
--- a/src/phenoml/openapi/openapi.json
+++ b/src/phenoml/openapi/openapi.json
@@ -2,7 +2,7 @@
"openapi": "3.0.3",
"info": {
"title": "Phenoml API",
- "version": "26224ef37e22adb507f28c21e8991d4a27822bde"
+ "version": "e555f47326197aad510a783942045e7c53476fc5"
},
"x-services": [
{
@@ -4586,7 +4586,7 @@
"post": {
"operationId": "batch_create",
"summary": "Create a batch job",
- "description": "Opens an empty batch job. Items arrive on later upload calls and the set\nis sealed at finalize.\n\nSupplying `request_id` makes the create idempotent on that token: a\nretried submit whose response was lost returns the original job rather\nthan opening a second one. This dedupe is scoped to the calling\ncredential. A `request_id` whose job was canceled or failed before it\nfinalized is released for a fresh replay; once a job is finalized, its\n`request_id` keeps resolving to it even after cancellation.\n\nAn instance may hold at most 4 active (pending or processing) jobs at\nonce; a create past that limit returns `409`. The limit is instance-wide\n\u2014 jobs are shared across the instance's credentials \u2014 so another\ncredential's jobs count against it.\n",
+ "description": "Opens an empty batch job. Items arrive on later upload calls and the set\nis sealed at finalize.\n\nSupplying `request_id` makes the create idempotent on that token: a\nretried submit whose response was lost returns the original job rather\nthan opening a second one. This dedupe is scoped to the calling\ncredential. A `request_id` whose job was canceled or failed before it\nfinalized is released for a fresh replay; once a job is finalized, its\n`request_id` keeps resolving to it even after cancellation.\n\nThere is no limit on how many jobs an instance may hold at once; how many\nitems run in parallel is a property of the instance, not of the job count.\n",
"requestBody": {
"required": true,
"content": {
@@ -4617,9 +4617,6 @@
"401": {
"description": "Unauthorized"
},
- "409": {
- "description": "The instance is at its 4-job active-batch limit; wait for one to finish"
- },
"499": {
"description": "Client closed request before response was ready"
},
@@ -4860,7 +4857,7 @@
"post": {
"operationId": "batch_cancel",
"summary": "Cancel a batch job",
- "description": "Drives a job to the terminal `canceled` state on request, freeing its\nactive-job slot immediately. Takes no request body.\n\nCancel does not delete the job: the job record and any results already\nproduced are preserved for the normal retention window, the same as a\n`completed` or `failed` job. Items stop being processed and keep the state\nthey held at cancellation, so a canceled job's `counts` may show\nunfinished items that never resolve.\n\nCancel is idempotent: canceling an already-`canceled` job returns `200`\nwith the job. Canceling a job that has already `completed` or `failed` is\na `409`.\n",
+ "description": "Drives a job to the terminal `canceled` state on request. Takes no\nrequest body.\n\nCancel does not delete the job: the job record and any results already\nproduced are preserved for the normal retention window, the same as a\n`completed` or `failed` job. Items stop being processed and keep the state\nthey held at cancellation, so a canceled job's `counts` may show\nunfinished items that never resolve.\n\nCancel is idempotent: canceling an already-`canceled` job returns `200`\nwith the job. Canceling a job that has already `completed` or `failed` is\na `409`.\n",
"parameters": [
{
"name": "job_id",
@@ -6574,11 +6571,11 @@
"tags": [
"FHIR Artifacts / Implementation Guides"
],
- "summary": "Delete an implementation guide's metadata",
- "description": "Deletes the stored metadata for an implementation guide \u2014 its\nprofile_context and timestamps. Member profiles keep their\nimplementation_guide assignment, so a guide still referenced by at least\none profile continues to appear in listings, just without context or\ntimestamps.\n",
+ "summary": "Delete an implementation guide family",
+ "description": "Deletes the stored name-level metadata and any exact canonical package\nversions beneath the guide. Legacy member profile assignments are not\nchanged.\n",
"responses": {
"204": {
- "description": "Implementation guide metadata successfully deleted"
+ "description": "Implementation guide family successfully deleted"
},
"400": {
"description": "The name is reserved or malformed"
@@ -6590,7 +6587,7 @@
"description": "Forbidden - custom profiles are only available on dedicated instances"
},
"404": {
- "description": "No implementation guide metadata exists for this name"
+ "description": "No implementation guide family exists for this name"
},
"500": {
"description": "Server error"
@@ -6604,6 +6601,117 @@
"x-service": "fhir_artifacts"
}
},
+ "/fhir/implementation-guides/{name}/versions": {
+ "parameters": [
+ {
+ "name": "name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ }
+ ],
+ "post": {
+ "operationId": "implementation-guides_createVersion",
+ "tags": [
+ "FHIR Artifacts / Implementation Guides"
+ ],
+ "summary": "Publish an exact custom-profile package",
+ "description": "Publishes an exact package beneath this guide family. PR 2 temporarily\npermits one exact package version per guide family; publishing another\nversion returns `409 Conflict` until multi-version package support lands.\n",
+ "requestBody": {
+ "required": true,
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/implementation-guides_CreateCanonicalImplementationGuideRequest"
+ }
+ }
+ }
+ },
+ "responses": {
+ "201": {
+ "description": "Canonical package published",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/implementation-guides_ImplementationGuideVersionDetail"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid ImplementationGuide, profile reference, version, or context"
+ },
+ "404": {
+ "description": "An exact profile reference was not found"
+ },
+ "409": {
+ "description": "A package family URL or exact version already exists"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "x-service": "fhir_artifacts"
+ }
+ },
+ "/fhir/implementation-guides/{name}/versions/{version}": {
+ "parameters": [
+ {
+ "name": "name",
+ "in": "path",
+ "required": true,
+ "schema": {
+ "type": "string"
+ }
+ },
+ {
+ "name": "version",
+ "in": "path",
+ "required": true,
+ "description": "The authored ImplementationGuide.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.\n",
+ "schema": {
+ "type": "string",
+ "pattern": "^[A-Za-z0-9._~+-]+$"
+ },
+ "example": "1.0.0"
+ }
+ ],
+ "get": {
+ "operationId": "implementation-guides_getVersion",
+ "tags": [
+ "FHIR Artifacts / Implementation Guides"
+ ],
+ "summary": "Get an exact custom-profile package",
+ "responses": {
+ "200": {
+ "description": "Exact canonical package",
+ "content": {
+ "application/json": {
+ "schema": {
+ "$ref": "#/components/schemas/implementation-guides_ImplementationGuideVersionDetail"
+ }
+ }
+ }
+ },
+ "400": {
+ "description": "Invalid guide name or version"
+ },
+ "404": {
+ "description": "Package version not found"
+ }
+ },
+ "security": [
+ {
+ "bearerAuth": []
+ }
+ ],
+ "x-service": "fhir_artifacts"
+ }
+ },
"/fhir/profiles": {
"get": {
"operationId": "profiles_list",
@@ -6832,6 +6940,9 @@
"404": {
"description": "Profile not found"
},
+ "409": {
+ "description": "Profile is pinned by an exact implementation guide package"
+ },
"500": {
"description": "Server error"
}
@@ -7050,6 +7161,9 @@
"404": {
"description": "Profile or version not found"
},
+ "409": {
+ "description": "Profile version is pinned by an exact implementation guide package"
+ },
"500": {
"description": "Server error"
}
@@ -11803,7 +11917,7 @@
"properties": {
"kind": {
"type": "string",
- "description": "Short stable token to branch on. Item-level kinds: `invalid_input`\n(the stored body was not a valid create/document request),\n`processing_failed` (the conversion failed), `budget_exceeded` (the\nitem ran past its time budget \u2014 600s for a document item, 450s for a\ncreate item), `result_too_large` (the result exceeded the storage\ncap), `input_unavailable` (the input could not be read), and\n`retries_exhausted` / `attempts_exhausted` (buried after too many\nfailed attempts).\nJob-level kinds: `timeout` (the job did not finish within 36 hours\nof creation).\n",
+ "description": "Short stable token to branch on. Item-level kinds: `invalid_input`\n(the stored body was not a valid create/document request),\n`processing_failed` (the conversion failed), `result_too_large` (the\nresult exceeded the storage cap), `input_unavailable` (the input\ncould not be read), and `retries_exhausted` / `attempts_exhausted`\n(buried after too many interrupted attempts; an item has no\nper-item time budget, but a single attempt is bounded by the\nplatform at 30 minutes, and an item cut off three times is buried).\nJob-level kinds: `timeout` (the job did not finish within 36 hours\nof creation).\n",
"example": "processing_failed"
},
"message": {
@@ -11838,7 +11952,7 @@
},
"status": {
"type": "string",
- "description": "Job status. `completed` means every item has finished \u2014 some may have\nfailed, so check `counts` for the split. `failed` is a whole-job\nfailure (the job could not run at all), distinct from individual item\nfailures, which never fail the job. `canceled` is a caller-requested\ncancellation via `POST /lang2fhir/batch/{job_id}/cancel`; like the\nother terminal states it frees the job's active-job slot and keeps any\nresults already produced readable for the retention window.\n",
+ "description": "Job status. `completed` means every item has finished \u2014 some may have\nfailed, so check `counts` for the split. `failed` is a whole-job\nfailure (the job could not run at all), distinct from individual item\nfailures, which never fail the job. `canceled` is a caller-requested\ncancellation via `POST /lang2fhir/batch/{job_id}/cancel`; like the\nother terminal states it keeps any results already produced readable\nfor the retention window.\n",
"enum": [
"pending",
"processing",
@@ -13056,7 +13170,7 @@
},
"implementation-guides_ImplementationGuideSummary": {
"type": "object",
- "description": "Metadata for an implementation guide. This is an instance-local grouping record, not a complete FHIR ImplementationGuide resource.\n",
+ "description": "Metadata for an implementation guide. Canonical fields are present only for published canonical packages; metadata-only legacy records omit them.\n",
"properties": {
"name": {
"type": "string",
@@ -13073,6 +13187,14 @@
"description": "The number of custom profiles in this implementation guide.",
"example": 3
},
+ "canonical_url": {
+ "type": "string",
+ "description": "Canonical FHIR ImplementationGuide URL, when the family has an exact package."
+ },
+ "version_count": {
+ "type": "integer",
+ "description": "Number of retained exact package versions."
+ },
"created_at": {
"type": "string",
"format": "date-time",
@@ -13119,6 +13241,125 @@
}
]
},
+ "implementation-guides_CreateCanonicalImplementationGuideRequest": {
+ "type": "object",
+ "required": [
+ "implementation_guide",
+ "profile_refs"
+ ],
+ "properties": {
+ "implementation_guide": {
+ "$ref": "#/components/schemas/implementation-guides_FHIRImplementationGuide"
+ },
+ "profile_refs": {
+ "type": "array",
+ "minItems": 1,
+ "maxItems": 250,
+ "description": "Exact canonical `url|version` references to builtin or custom profiles. A package can contain at most 250 references.\n",
+ "items": {
+ "type": "string"
+ }
+ },
+ "profile_context": {
+ "type": "string",
+ "maxLength": 2000,
+ "description": "Natural-language profile-selection context for this package."
+ }
+ }
+ },
+ "implementation-guides_ImplementationGuideVersionDetail": {
+ "type": "object",
+ "required": [
+ "name",
+ "url",
+ "version",
+ "profile_context",
+ "profiles",
+ "profile_refs",
+ "implementation_guide",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "name": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "profile_context": {
+ "type": "string"
+ },
+ "profiles": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "profile_refs": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ },
+ "implementation_guide": {
+ "$ref": "#/components/schemas/implementation-guides_FHIRImplementationGuide"
+ },
+ "created_at": {
+ "type": "string",
+ "format": "date-time"
+ },
+ "updated_at": {
+ "type": "string",
+ "format": "date-time"
+ }
+ }
+ },
+ "implementation-guides_FHIRImplementationGuide": {
+ "type": "object",
+ "description": "A complete authored FHIR ImplementationGuide JSON resource.",
+ "required": [
+ "resourceType",
+ "url",
+ "version"
+ ],
+ "additionalProperties": true,
+ "properties": {
+ "resourceType": {
+ "type": "string",
+ "enum": [
+ "ImplementationGuide"
+ ]
+ },
+ "id": {
+ "type": "string"
+ },
+ "url": {
+ "type": "string"
+ },
+ "version": {
+ "type": "string"
+ },
+ "name": {
+ "type": "string"
+ },
+ "status": {
+ "type": "string"
+ },
+ "packageId": {
+ "type": "string"
+ },
+ "fhirVersion": {
+ "type": "array",
+ "items": {
+ "type": "string"
+ }
+ }
+ }
+ },
"implementation-guides_UpdateImplementationGuideRequest": {
"type": "object",
"properties": {