From a1de5c203813703bc576016e086eb4aee99ca74d Mon Sep 17 00:00:00 2001 From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:36:23 +0000 Subject: [PATCH 1/4] [fern-generated] Update SDK Generated by Fern CLI Version: unknown Generators: - fernapi/fern-python-sdk: 5.18.1 --- .fern/metadata.json | 6 +- poetry.lock | 258 +-- pyproject.toml | 5 +- reference.md | 1173 +++++++++- src/phenoml/__init__.py | 3 + src/phenoml/client.py | 19 + src/phenoml/core/client_wrapper.py | 2 +- src/phenoml/lang2fhir_batch/__init__.py | 93 + src/phenoml/lang2fhir_batch/client.py | 898 ++++++++ .../lang2fhir_batch/errors/__init__.py | 59 + .../errors/bad_request_error.py | 10 + .../errors/client_closed_request_error.py | 10 + .../lang2fhir_batch/errors/conflict_error.py | 10 + .../errors/content_too_large_error.py | 10 + .../errors/gateway_timeout_error.py | 10 + .../errors/internal_server_error.py | 10 + .../lang2fhir_batch/errors/not_found_error.py | 10 + .../errors/unauthorized_error.py | 10 + src/phenoml/lang2fhir_batch/raw_client.py | 2001 +++++++++++++++++ src/phenoml/lang2fhir_batch/types/__init__.py | 65 + .../lang2fhir_batch/types/batch_counts.py | 27 + .../lang2fhir_batch/types/batch_error.py | 40 + .../types/batch_item_status.py | 64 + .../types/batch_item_status_status.py | 5 + .../lang2fhir_batch/types/batch_job.py | 73 + .../lang2fhir_batch/types/batch_job_status.py | 5 + .../types/job_detail_response.py | 36 + .../types/job_list_response.py | 33 + .../types/results_page_response.py | 33 + .../types/upload_item_response.py | 40 + src/phenoml/profiles/__init__.py | 21 +- src/phenoml/profiles/client.py | 19 + src/phenoml/profiles/errors/__init__.py | 11 +- src/phenoml/profiles/errors/conflict_error.py | 10 + src/phenoml/profiles/profiles/client.py | 142 +- src/phenoml/profiles/profiles/raw_client.py | 85 +- src/phenoml/profiles/types/__init__.py | 6 + .../profiles/types/profile_get_response.py | 6 +- .../profiles/types/profile_list_response.py | 2 +- src/phenoml/profiles/types/profile_summary.py | 42 +- .../types/profile_version_create_request.py | 8 + .../types/profile_version_list_response.py | 20 + src/phenoml/profiles/versions/__init__.py | 4 + src/phenoml/profiles/versions/client.py | 392 ++++ src/phenoml/profiles/versions/raw_client.py | 874 +++++++ tests/wire/test_lang2FhirBatch.py | 78 + tests/wire/test_profiles_profiles.py | 32 +- tests/wire/test_profiles_versions.py | 44 + wiremock/wiremock-mappings.json | 375 ++- 49 files changed, 6944 insertions(+), 245 deletions(-) create mode 100644 src/phenoml/lang2fhir_batch/__init__.py create mode 100644 src/phenoml/lang2fhir_batch/client.py create mode 100644 src/phenoml/lang2fhir_batch/errors/__init__.py create mode 100644 src/phenoml/lang2fhir_batch/errors/bad_request_error.py create mode 100644 src/phenoml/lang2fhir_batch/errors/client_closed_request_error.py create mode 100644 src/phenoml/lang2fhir_batch/errors/conflict_error.py create mode 100644 src/phenoml/lang2fhir_batch/errors/content_too_large_error.py create mode 100644 src/phenoml/lang2fhir_batch/errors/gateway_timeout_error.py create mode 100644 src/phenoml/lang2fhir_batch/errors/internal_server_error.py create mode 100644 src/phenoml/lang2fhir_batch/errors/not_found_error.py create mode 100644 src/phenoml/lang2fhir_batch/errors/unauthorized_error.py create mode 100644 src/phenoml/lang2fhir_batch/raw_client.py create mode 100644 src/phenoml/lang2fhir_batch/types/__init__.py create mode 100644 src/phenoml/lang2fhir_batch/types/batch_counts.py create mode 100644 src/phenoml/lang2fhir_batch/types/batch_error.py create mode 100644 src/phenoml/lang2fhir_batch/types/batch_item_status.py create mode 100644 src/phenoml/lang2fhir_batch/types/batch_item_status_status.py create mode 100644 src/phenoml/lang2fhir_batch/types/batch_job.py create mode 100644 src/phenoml/lang2fhir_batch/types/batch_job_status.py create mode 100644 src/phenoml/lang2fhir_batch/types/job_detail_response.py create mode 100644 src/phenoml/lang2fhir_batch/types/job_list_response.py create mode 100644 src/phenoml/lang2fhir_batch/types/results_page_response.py create mode 100644 src/phenoml/lang2fhir_batch/types/upload_item_response.py create mode 100644 src/phenoml/profiles/errors/conflict_error.py create mode 100644 src/phenoml/profiles/types/profile_version_create_request.py create mode 100644 src/phenoml/profiles/types/profile_version_list_response.py create mode 100644 src/phenoml/profiles/versions/__init__.py create mode 100644 src/phenoml/profiles/versions/client.py create mode 100644 src/phenoml/profiles/versions/raw_client.py create mode 100644 tests/wire/test_lang2FhirBatch.py create mode 100644 tests/wire/test_profiles_versions.py diff --git a/.fern/metadata.json b/.fern/metadata.json index 50a967f..1241008 100644 --- a/.fern/metadata.json +++ b/.fern/metadata.json @@ -1,5 +1,5 @@ { - "cliVersion": "5.106.0", + "cliVersion": "5.114.0", "generatorName": "fernapi/fern-python-sdk", "generatorVersion": "5.18.1", "generatorConfig": { @@ -8,10 +8,10 @@ "enabled": true } }, - "originGitCommit": "4a08550f5db230949c7423d0ce5aa7055e8f0d65", + "originGitCommit": "e2cfa711e7e5ad04621efc35d12dcfd7d060040d", "originGitCommitIsDirty": true, "invokedBy": "ci", "requestedVersion": "AUTO", "ciProvider": "unknown", - "sdkVersion": "16.11.0" + "sdkVersion": "0.0.0.dev0" } \ No newline at end of file diff --git a/poetry.lock b/poetry.lock index c3a2b77..435fa6a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -188,20 +188,20 @@ files = [ [[package]] name = "anyio" -version = "4.14.2" +version = "4.15.0" description = "High-level concurrency and networking framework on top of asyncio or Trio" optional = false python-versions = ">=3.10" groups = ["main"] files = [ - {file = "anyio-4.14.2-py3-none-any.whl", hash = "sha256:9f505dda5ac9f0c8309b5e8bd445a8c2bf7246f3ce950121e45ea15bc41d1494"}, - {file = "anyio-4.14.2.tar.gz", hash = "sha256:cfa139f3ed1a23ee8f88a145ddb5ac7605b8bbfd8592baacd7ce3d8bb4313c7f"}, + {file = "anyio-4.15.0-py3-none-any.whl", hash = "sha256:7ecd9937369ffce8bba0b5ccb9b3a9507b101b0ed50256aecfbab27e6c2acb99"}, + {file = "anyio-4.15.0.tar.gz", hash = "sha256:b5c620ed540725e2579c31b17bb995b3bf02c9281c9cace04c7d186380bab85e"}, ] [package.dependencies] exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} idna = ">=2.8" -typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""} +typing_extensions = {version = ">=4.16.0", markers = "python_version < \"3.15\""} [package.extras] trio = ["trio (>=0.32.0)"] @@ -1118,19 +1118,19 @@ files = [ [[package]] name = "pydantic" -version = "2.13.4" +version = "2.13.5" description = "Data validation using Python type hints" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"}, - {file = "pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6"}, + {file = "pydantic-2.13.5-py3-none-any.whl", hash = "sha256:346a034f080da3755d8e9cb5e00e8b07de1d39e4f6e2c87d8ab7cafa0b269a73"}, + {file = "pydantic-2.13.5.tar.gz", hash = "sha256:51a9c5f7b2f8e636f04c6cada605d9b6a3bf1348fdf945a3d8869b19bba0ee08"}, ] [package.dependencies] annotated-types = ">=0.6.0" -pydantic-core = "2.46.4" +pydantic-core = "2.46.5" typing-extensions = ">=4.14.1" typing-inspection = ">=0.4.2" @@ -1140,132 +1140,132 @@ timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows [[package]] name = "pydantic-core" -version = "2.46.4" +version = "2.46.5" description = "Core functionality for Pydantic validation and serialization" optional = false python-versions = ">=3.9" groups = ["main"] files = [ - {file = "pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4"}, - {file = "pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01"}, - {file = "pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f"}, - {file = "pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39"}, - {file = "pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d"}, - {file = "pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf"}, - {file = "pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594"}, - {file = "pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398"}, - {file = "pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3"}, - {file = "pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33"}, - {file = "pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d"}, - {file = "pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2"}, - {file = "pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987"}, - {file = "pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b"}, - {file = "pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89"}, - {file = "pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a"}, - {file = "pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008"}, - {file = "pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262"}, - {file = "pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be"}, - {file = "pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292"}, - {file = "pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d"}, - {file = "pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb"}, - {file = "pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c"}, - {file = "pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e"}, - {file = "pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac"}, - {file = "pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5"}, - {file = "pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596"}, - {file = "pydantic_core-2.46.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:fd8b3d9fd264be37976686c7f65cd52a83f5e84f4bfd2adf9c1d469676bbb6ae"}, - {file = "pydantic_core-2.46.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9f444c499b3eefd3a92e348059471ea0c3a6e303d9c1cec09fa748fd9f895201"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3447661d99f75a3683a4cf5c87da72f2161964611864dbbeac7fbb118bb4bfc0"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8b9bab013d1c7a79d3501ff86d0bc9c31bf587db4551677b96bec07df78c6b15"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d995260fdf4e1db774581b4900e0f832abe3c7c84996726bbc161b19c8f29e76"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f13a646d65d09fbf1bc6b3a9635d30095c8e7e5cc419ff35ecc563c5fd04cd49"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432c179df7874eeb73307aad2df0755e1ae0efa61ff0ea89b93e194411ae3928"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:e68b7a074f65a2fd746c52a7ce6142ab7006074ac269ace0c25cd8ba171f8066"}, - {file = "pydantic_core-2.46.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4a05d69cba51d852c5c3e92758653245a50c0b646ced0cf05bd793ed592839d6"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:228ee9bae8bef5b1e97ec58302f80357c37199e0d0a99174e138d28e6957b9d9"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:10e17cbb10a330363733efc4d7c4d0dd827ac0909b8f6a6542298fed1ea62f29"}, - {file = "pydantic_core-2.46.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:91a06d2e259ecfbd8c901d70c3c507900458498142b3026a296b7de4d1322cc9"}, - {file = "pydantic_core-2.46.4-cp39-cp39-win32.whl", hash = "sha256:d80ee3d731373b24cebbc10d689ca4ee1875caf0d5703a245db18efd4dd37fc1"}, - {file = "pydantic_core-2.46.4-cp39-cp39-win_amd64.whl", hash = "sha256:3be77f45df024d789a672ae34f8b06fb346c4f9f46ea714956660ea4862e89ac"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b"}, - {file = "pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526"}, - {file = "pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc"}, - {file = "pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983"}, - {file = "pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:657b40d6240c0a7b6a64b30f22d1e3aa631c7e846c621b0c0f6d1d75e2e15ea6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ecb42011e12ee19cafbc312887cbf3546959fe02fbad44f272d4be5baa997615"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4dedce55295becb61921e386b99d4f2706045306e7fa52249a33004c837379fb"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9f47b8a949e60f027f0aa0a6f6c7b7e9c55cbf4380d10b344e282fa4e7ab1e1b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:200aa3dc9f8d54f0754f43247c0bad0999fdcfbfd2488384dd44f37279271fe6"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6d30e1a4f138b8951063e9a394752a9179b51da288ffa507b1e659222f4c1793"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:850a08d167dde16db8702c274f320c7be9d7da6f6dff2b58b18f9e815bd94f5b"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:c3471e5c4a949c26ec00a77f01df59096aa9495877de76fd60a980f8ee6be461"}, + {file = "pydantic_core-2.46.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:3a3e26b6a8274211bddee2d0e4d0d42778f17a34510f49d2ec44b58abfc41736"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:fc5d783bd4a2387e97b8a2d5ec781cfb92b3d893bf82370548e99db5915935d3"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:356c8368cbc321050b169595683a2e1d63413b1e0e2868b330af9fc14c616d3f"}, + {file = "pydantic_core-2.46.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:eb7d8d0e5886a89a55d2eef490e272fa965a9d57c6b29a5b5088a7997ec2cad1"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win32.whl", hash = "sha256:4d44cf99ddebf875f9b68cc267aa684c99b7b44fe63ee1cac4ec163807290069"}, + {file = "pydantic_core-2.46.5-cp310-cp310-win_amd64.whl", hash = "sha256:1e5aad1220a1192c42341c8fd4a8686657e73ab2a920c970bdc4de334fe3193d"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a1dee1b804ff4d11c663636cf15d2ea47e9f79cd56c033fb1cbf08924842a48f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d625a186a65201c23a9e3b8ed9c47e90a026e03256608cc91851c6709096844f"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4f8507560a9284e1370bb048ed4282012fbef4e8d109875b95e884d228552061"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f93c5fe914d75fbec9a49209b00da5f08e9e467d69da2b1510c81940cfd10be"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aca6c767f552b21b10f774aeac128e828eafb796adfa1b666a18bf6321453c3a"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:701b2e04b560eeb4bddf7a25ab8ca476176e34fdbd9a0e18196f0d12d4685f0b"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49776eab08766a08dfff7012f8b422dcd7e25e43b316eedf0477c24fcfa84b7c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:a2468d93d181667a7abd66e1b64bb9f76f361b0fef8faddf687456453576f5ee"}, + {file = "pydantic_core-2.46.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:53feb344243bb9510a9dec7bf3cf1b64d88a98af5dc7872a5160465f8b198c8e"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cd5214352ae68f3b5e9af7768bdc5253695ee069675db3480518420b3be881f2"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:9432f3598db432cb51c5b37fdbf29a60fcccc79e30d37a05022776a6bc4ab689"}, + {file = "pydantic_core-2.46.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:8feeac04b5794e513e710af2f9c87d49f31a6dc47967bb264a1fed61a8989bec"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win32.whl", hash = "sha256:892a881d5f68c2b9ea304b7a6c2c60d9343df578a311b0f86b94bc8f1ffe8129"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_amd64.whl", hash = "sha256:40375c2d05acec10323e45dfe2077ac44bc74659008614af5069034e2cfc781c"}, + {file = "pydantic_core-2.46.5-cp311-cp311-win_arm64.whl", hash = "sha256:28a6a556cd3b6066bea827857f9d9cce027c96f776e512f544a581f9e42161f8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b9fe6fb92520e3fd61f2e49000b6911b188824f089b75973ea06d6267f0b476d"}, + {file = "pydantic_core-2.46.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a39ac25a9a2fa4072efdb429833c4a4c8009a51ff9eea3eeae131713cd27991e"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4fdc8b93a41521988916eeaa271173fcca7fa0803d62f87675aac8dcec1c8e29"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b98134087d9de723658d17a42c7d0da8d6e2ef08015dee7dc93889047315f5e4"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e652ab17569c94bff5475520f907b7148b8c24036a8ebbe5cf7cf7493d28579a"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d925f3d9afd05a8c0fb3a1031463a8d59ebe5e2afad297e29c78be19e13b4e62"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0fc5be0abd4a407e200d844b404e33639a554e7bd0d448e7b9ae181be4789ac2"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:816ff0a6550ffc06c098ccd2e0698600f9aa7da192a79eaa6f9af504a35db869"}, + {file = "pydantic_core-2.46.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c7ea57fc63aa7da93a1bd2d644e6577befae10c52c4e36377635eea1056a74f5"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:efd62a42486f1bda5d24cb4f63d15a3c7768375fe83d36f9417b4ad7a2fb20b3"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2bc9419666990c06d7397831f2126a1ecc3594aaa3ff7de5bf2d066802f4e07b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:18a09e1e1011b462f2e32774f25859ef1223d5c2b0546a633cf56654710721e0"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win32.whl", hash = "sha256:5cb482e9e84c851f4e623fe4acc1ced89168cf1fe18f7089db4548c8f5bbb65b"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_amd64.whl", hash = "sha256:5e81740c09e310f5aa5cbd3e434a01c154d4bef93241c7877b39f211d2b78ba8"}, + {file = "pydantic_core-2.46.5-cp312-cp312-win_arm64.whl", hash = "sha256:f7b0ec93a2893de856652154d73b7ba622f26fa97726487dcac373de5f4c6084"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:b7ca9034437b6022f941f4857459562ee00a560b97e7cce8a0ec5a74fc6766e0"}, + {file = "pydantic_core-2.46.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f332f0e72a5a0400141f830744e141bf9f97917878dbe968669e8a7fefea78ff"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:193375f3548919d3f0b60936ca113ada3e38f264f91b9b8e0508efaad57be931"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:79bdfa52f843137045b2d081cc05c120ba6665d29b7559c2c47690906f39279f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:24922243639cbdac66c75fcb6fd6495a9cb52b213d62f9a0d16f0310b1ff8038"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c76fe65e607be28c7fd4d56fc3c42b1583aa058ce3408b7ad0fd540171d31f9f"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6f7b393a8b3da82f5c1fc0751e6d01ac6c55b93c18226a60bdfba4a724efafd1"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:7ac031912d54f3d83ef3b3eb98dfabc1608802e2202263d25957eeed40b94761"}, + {file = "pydantic_core-2.46.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:837b396ca3d7b74091ca623f6cbd8351bd42d670a79c2683e79fb089f06a2de5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:5ee239d575f80b08eca11f6e20f90c4c695de7825c67eefe6091fbf20dda648e"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:e80675d75ae2cd14372cb65cad5400d9347a3d3f6c13000183f22dfd027283ed"}, + {file = "pydantic_core-2.46.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:9c4b71f10dd532fb7a5cbc8f58707779e64f03a258c2bf8bfbaecfcd9970b519"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win32.whl", hash = "sha256:97bf8de4d541598c94a59344eeb988a94c08ff76b5723c41f6567ec18c7892ea"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_amd64.whl", hash = "sha256:15f4a94963c95accac15b7b657bb177d3ad82bb90b0d0526d9a9b85079925db5"}, + {file = "pydantic_core-2.46.5-cp313-cp313-win_arm64.whl", hash = "sha256:d22a945598fb91236b4dd793a6e42e4f3dd7740bb5aace5ebd7d4c08d13bb575"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:c1c43ad4339643d70ebb8124e1305a7dab423001eff58bb41a0f731adbc98355"}, + {file = "pydantic_core-2.46.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1a353f84de772f423b5ffb11d7ae352fbbef0f446f3c0b0af0f8236d7233606e"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5086029a57366b8cf81b130a43908738095c270c21a8d7f0e8bdfdb89718e2f3"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46c25dda9d092a06c08db76ffe0a197107904d0dfac653f7d5306bbcd6d6119c"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:37ea7b83c935e5b0d68c9449b82651accf78a10828b2c02b2f2d9e9496446c21"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e64e88d5585bea9ce95861079de72006c7fa6d3df4e3a3b65ba31eb979c15c9f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:54d510bac3ee52247af28ed4bb18a1e799f040ac60fd2bf5ccd4c92f1fbe786f"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:a2a5e1d0ff29adddc9f6d6821a66302e4493f8ca898b715b6b1182c2c201ea0a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:03b9666e41e35d8909852ba191a0607520f81b74eaf12ccf8737005dbb313821"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:a91c17edf6eea2402cb5457b4c89e99bc5ed1004aa34c4adf1d4258c1a5c22c2"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b49924c73a235e969511bf2aabdff3beebf9820931f646c80274d5d780010c47"}, + {file = "pydantic_core-2.46.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:2cbd9a5eff05e51c447c34dfa4632145b26b09120cf04bd0c871e44c1a5e1c9a"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win32.whl", hash = "sha256:2d5d76654becf5efd62c9e51c3756c67b49498b0c9a40884934c40807adbd074"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_amd64.whl", hash = "sha256:fa10ef4112775900e7a0661068635eb67b2ab824fbde764de6e0e21982a93db0"}, + {file = "pydantic_core-2.46.5-cp314-cp314-win_arm64.whl", hash = "sha256:045ab3b6d308439e32b81cc173bba5b9018bc6ed896afd0c65b3b009b1699af5"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8816f3d218beb4b787de5c9759c259b8fa61f9dec42dc7811f320a33771778b7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:bce57638e08ac148e5778cce7feb968307a727d66f8e2274a543d0cf0c9ad6a3"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:976e1128455aa595ea04c79ccfedff1aaeab96ee013fcc916bed120c4f0ad94f"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b891faeedeafba41b2983e5001a81b6a915b69544c7e7570d1989ce1c36ac7"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5f194189415698233dd1114a093a9b56e61e2c57e11b469be3b0506f46f0771c"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82a36973cf8a2ef5406f4fe2edbf8ed0c99629535d959e0b100c76a32535a111"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cdbb78909f52b981d3b2d56b97328d71eb0b974c36bd77c920123a7ebb192829"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:52e24eacdb536cade636aa90fb851835222becff8484b7001fdc78cb0290f2aa"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:37ae34309d7bd8c0d61ab839668058f2a7962ea1fc51d105d2db228fe0618034"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:0cdbada856a1c69a7624a64d3d9aefe79300bd6ef827b43a4f265010b9b55184"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:545f26c504b27c3758439a5e6d9349931f0a04f855668d5fe323c89e82300a38"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:ff218293c9c806138dca139765e3b067621be52bcd93cdc14c7711be7ddc90a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win32.whl", hash = "sha256:97cf3eb53a8cccacf9d46686a0926186c9bfb5574f2ed66d3639d5fe117cd3a9"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_amd64.whl", hash = "sha256:d2f9fc07a8042a8f95925b35c4f04f469707c981fc33245b6ca187cf5d2dd290"}, + {file = "pydantic_core-2.46.5-cp314-cp314t-win_arm64.whl", hash = "sha256:acf8a67ba51f4ca9ddbd0e6b3000a65ac51ab734661778b3e7ba64d99a710f2f"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:c583b927a8838dab890706a6fa7573fbb8b70e24000ef9f7238e2d6f6435a5ed"}, + {file = "pydantic_core-2.46.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:cdc8b74ecc48c0cb1e9607a05ec4e9e88db60a19ffcc9a1d5f9088ede40c8dc0"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b10e3e8fd7ddc2bd915848a2768e44c15b22936f1cc54c462ad1164deb02655"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f077d0b97ab11fa7dcc633fca53515f290bca8a8a633e966d5b6d1879d9ed01a"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b0fc826b16c55e561e5d2a0c5c77b051ba1d92808118c4e4b5390f5e0cf191d"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ef3fbbf161dc9351a2fe0422e51b129f9e97e42385bd0320b309c15f7d287dd8"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:978e7b97d4824b5be09c69fb70507cbde3b0323fc147332ca40a94d9a6a0ebbf"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_31_riscv64.whl", hash = "sha256:9b68938dd5b0c783d88ff8e2dcc69451b5eb936fe212d516b21b9d5567f6d464"}, + {file = "pydantic_core-2.46.5-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:771cf63ae0b1b50dd22e5f3e3549fab5f3f4ff1635d352a9e1a97fe01c7b2e64"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7c6be839a5a8312626b32029a415644a0846b420bc8b52b95b28cd92da162168"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_armv7l.whl", hash = "sha256:895395f8918627b04efb1ad2a4cf605387143300ba03304cd1dfa6d03f5e095e"}, + {file = "pydantic_core-2.46.5-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:fc8515076c11f3cfdf4fb142dcca0fe384b1230a3b5415458ac84f3e0903ec13"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win32.whl", hash = "sha256:3d2652072b2d774947ba5cf78a9e59644ac62ee572daf6dd2e1dfe905e15b2b7"}, + {file = "pydantic_core-2.46.5-cp39-cp39-win_amd64.whl", hash = "sha256:3aa166e99c4f2985407fb8714aebede877ecb5455cf321b606adca926d30d5a0"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:c14ad3bdc85ee7f318742c457ca3968a92126d144b15721c759033bfb06296c2"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0bddb4020d8f04175865ccd17eff3040874fc11fb593f424edb452653b4b947c"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2471fd51c61c610e1dcf7de44d7299283661654d11264ab4802b303368d69c47"}, + {file = "pydantic_core-2.46.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b10ec717381bdbfafef34607824db4c91de69ff085e4fca3b2af91b4fa17e68a"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:013d6f3483d81e02e7c328831808f336c8596ee33b4bd4026b9ffb1e960b8942"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:e9c134bb666dd54b778b9fc0d2b50cbb7f979b9e3716f26a88c9ab3b6fc1dd0f"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:347ec774390c87326a2e4929d58d3f7e8763a104d5d35f4cd595a4c952366433"}, + {file = "pydantic_core-2.46.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8e24d8f05fa2d28513d94e877e9c75ad66175376209b3977f916e240e623193c"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:ab4b66edffb32d9e951efb3814bd104b8367a7501b81b955cacb5726d897389f"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:337639ba62a11acde6ef3aeb08c8ea755f8ef1fe5e513356c0f36a2b0d7568b0"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:413a717a410d0c817ef5b786a059415550b3794e1d0c2abffd9efb93a3d9f7b4"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1e449def1945a462c464331254e5a44fca7c3b4f9aedf59ec2f50f8066dd8e25"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:a445486499897b88a7d6c310c88ed64dd37b1b59bfd7ae9107490bbb362f47d6"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:2d330aaba8621b1edcec8ae2c4050f63b84ccf6d98723a8f212e9684713abf0e"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:b6acfb46a814762367fb7ba0828b0a17d441b92ce249a0e007474c9072662dda"}, + {file = "pydantic_core-2.46.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d0a24b40877af2de4950252be9d21eaf7fb07660f3c2cae1f56c6b599ada5266"}, + {file = "pydantic_core-2.46.5.tar.gz", hash = "sha256:10416c15b8839ecc4ef4d0885da76da6fd0f67333a0eb8aff6d93c4b8f2910fc"}, ] [package.dependencies] diff --git a/pyproject.toml b/pyproject.toml index f599fde..cea8b20 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "phenoml" -version = "16.11.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' diff --git a/reference.md b/reference.md index 0c4d8d9..565e109 100644 --- a/reference.md +++ b/reference.md @@ -5427,6 +5427,730 @@ File type is auto-detected from content magic bytes. + + + + +## Lang2FhirBatch +
client.lang2fhir_batch.list(...) -> JobListResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns a page of the instance's batch jobs, newest first, without +per-job counts. Jobs are shared across the instance's credentials, so +this lists every batch job on the instance, not just the calling +credential's. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.lang2fhir_batch.list( + cursor="cursor", + limit=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous page's next_cursor. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 20; values above 100 are clamped to 100. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.lang2fhir_batch.create(...) -> BatchJob +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Opens an empty batch job. Items arrive on later upload calls and the set +is sealed at finalize. + +Supplying `request_id` makes the create idempotent on that token: a +retried submit whose response was lost returns the original job rather +than opening a second one. This dedupe is scoped to the calling +credential. + +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. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.lang2fhir_batch.create( + request_id="submit-2025-09-02-batch-001", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request_id:** `typing.Optional[str]` + +Optional client idempotency token. A retried create with the same +token returns the original job instead of opening a second one. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.lang2fhir_batch.upload_item(...) -> UploadItemResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Stores one item of a job from a multipart upload. A batch's items arrive +one per request. The item carries **either** a `document` extraction +(whose input file rides as raw bytes in the `file` part) **or** a +`create` extraction (JSON only, no file). + +The upload enforces these rules: +- Set **exactly one** of `document` or `create`. Setting both, or + neither, is a `400`. +- When `document` is set, `file` is **required** — it supplies the + document's binary content (PDF or image). +- When `create` is set, `file` is **forbidden** — a create item carries + no file. +- `document` and `create` must each be a JSON **object**. + +Only the item's structure is checked here: the fields inside `document` +or `create` are not validated at upload. A body that is well-formed JSON +but not a valid request for its endpoint is still accepted with `202` +and fails later during processing, recorded as an item `error`. A +wrong-typed field the endpoint cannot decode fails as `invalid_input`; a +body that decodes but the pipeline rejects (for example, a missing +required field) fails as `processing_failed`. + +Supplying `request_id` makes the upload idempotent on that token. A +re-upload under the same token overwrites the same item rather than +adding a second, so a client that lost an upload's response can safely +re-send it. The response's `deduplicated` is `true` only when the +re-uploaded payload matches the one already stored; a same-token upload +with a changed payload overwrites in place and returns `false`. + +Set a `request_id` on **every** upload: re-sending under the same token +is the only way to repair a lost or incomplete upload, including the one +a finalize `409` reports. Without one, a re-send adds a new item instead +of replacing the missing one, and the job cannot be finalized. + +Uploads are rejected once the job has been finalized (`409`), once it +holds its 500-item limit (`409`), or when the item is too large (`413` — +see the raw-file limit in the API description). +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.lang2fhir_batch.upload_item( + job_id="job_id", + file="example_file", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**job_id:** `str` + +
+
+ +
+
+ +**document:** `typing.Optional[typing.Dict[str, typing.Any]]` + +The JSON body of `POST /lang2fhir/document/multi`, **without** +its base64 `content` field — the uploaded `file` supplies the +content. Accepts that endpoint's fields (`version`, `provider`, +`patient_reference`, `implementation_guide`, `detection_effort`, +`validation_method`, `config`). This is the **multi**-resource +body: it has no single-`resource` field, and the item's result +is a `DocumentMultiResponse` (a Bundle of resources). Mutually +exclusive with `create`; requires `file`. + +
+
+ +
+
+ +**create:** `typing.Optional[typing.Dict[str, typing.Any]]` + +The JSON body of `POST /lang2fhir/create/multi`. Accepts that +endpoint's fields (`text`, `version`, `provider`, +`patient_reference`, `implementation_guide`, `detection_effort`, +`validation_method`, `resource_review`). This is the +**multi**-resource body: it has no single-`resource` field, and +the item's result is a `CreateMultiResponse` (a Bundle of +resources). Mutually exclusive with `document`; must **not** be +accompanied by a `file`. + +
+
+ +
+
+ +**file:** `typing.Optional[core.File]` + +The document's binary content (PDF, PNG, JPEG, or TIFF). +Required with `document`; forbidden with `create`. + +
+
+ +
+
+ +**request_id:** `typing.Optional[str]` + +Optional idempotency token (max 256 bytes). Re-uploading under +the same token overwrites the same item instead of adding a +new one. The token is scoped to this job; the same token in +another job is independent and creates a separate item. + +
+
+ +
+
+ +**id:** `typing.Optional[str]` + +Optional caller-supplied correlation label (max 512 bytes), +echoed back on status and result listings so you can match the +server's item_id to your own record. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.lang2fhir_batch.finalize(...) -> BatchJob +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Seals the job's item set and starts processing. Takes no request body. +Finalize is idempotent: a retried finalize succeeds again. + +If a previous upload did not complete, finalize returns a `409`; re-send +the missing upload (with the same `request_id`), then finalize. +Finalizing a job with no items is a `400`. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.lang2fhir_batch.finalize( + job_id="job_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**job_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.lang2fhir_batch.get(...) -> JobDetailResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Returns a job's record, its per-status item counts, and one page of +per-item statuses. + +Items are listed in a stable order that is not upload order and is the +same across pages. Match each entry to your own records by its `id` +(your correlation label) or `item_id` (from the upload response), +never by position. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.lang2fhir_batch.get( + job_id="job_id", + cursor="cursor", + limit=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**job_id:** `str` + +
+
+ +
+
+ +**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous page's next_cursor. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.lang2fhir_batch.get_results(...) -> ResultsPageResponse +
+
+ +#### 📝 Description + +
+
+ +
+
+ +A lighter status page. Returns the same per-item status entries as +`GET /lang2fhir/batch/{job_id}`, but without the job record or counts, +and the entries carry `result_size` rather than any result content. Use +each entry's `item_id` to fetch that item's result from +`GET /lang2fhir/batch/{job_id}/results/{item_id}`. + +Entries are listed in a stable order that is not upload order and is +the same across pages. Match each entry to your own records by its `id` +(your correlation label) or `item_id` (from the upload response), +never by position. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.lang2fhir_batch.get_results( + job_id="job_id", + cursor="cursor", + limit=1, +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**job_id:** `str` + +
+
+ +
+
+ +**cursor:** `typing.Optional[str]` — Opaque pagination cursor from a previous page's next_cursor. + +
+
+ +
+
+ +**limit:** `typing.Optional[int]` — Page size. Defaults to 20; values above 100 are clamped to 100. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.lang2fhir_batch.get_result(...) -> typing.Dict[str, typing.Any] +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Streams one item's stored result bytes verbatim as `application/json`. +The body is the response the item's synchronous multi endpoint would have +returned — a `DocumentMultiResponse` for a document item or a +`CreateMultiResponse` for a create item. + +Only a succeeded item has a result: an item that has not succeeded +(pending, processing, or failed) is a `409`, and a result that has +expired is a `404`. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.lang2fhir_batch.get_result( + job_id="job_id", + item_id="item_id", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**job_id:** `str` + +
+
+ +
+
+ +**item_id:** `str` + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ +
@@ -5436,22 +6160,194 @@ File type is auto-detected from content magic bytes.
-#### 📝 Description +#### 📝 Description + +
+
+ +
+
+ +Returns metadata for every custom (uploaded) FHIR profile on this +instance, across all implementation guides. The full StructureDefinition +JSON is omitted from each entry; fetch a single profile by id to retrieve it. + +The `url` query parameter filters by canonical URL. The canonical URL is the +stable key other platform features use to reference a profile (FHIR's +`meta.profile`, `baseDefinition`), since StructureDefinition ids are only +unique within a package. An unpinned `url` filter returns metadata for +the profile's current StructureDefinition. Pinned `url|version` filters +resolve a retained version when present; otherwise they can fall back to +the profile's current StructureDefinition, whose content can change +through the profile update endpoint. A non-matching filter returns an +empty list, not a 404. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.profiles.profiles.list( + url="http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**url:** `typing.Optional[str]` — Filter by canonical URL. Accepts the FHIR pinned form `url|version`; without a version pin, returns the profile's current StructureDefinition metadata. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+ + +
client.profiles.profiles.create(...) -> ProfileSummary +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Creates a custom profile from a FHIR StructureDefinition supplied as a JSON +object. Metadata such as version, resource type, and url is read from the +StructureDefinition; the lowercase StructureDefinition id becomes the +profile's lookup key. When id is omitted, a random UUID is assigned. +Optionally group the profile under a named implementation guide. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.profiles.profiles.create( + structure_definition={ + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": False, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": {"element": [{"id": "Patient", "path": "Patient", "min": 0, "max": "*"}]} + }, + implementation_guide="acme-cardiology", +) + +``` +
+
+
+
+ +#### ⚙️ Parameters + +
+
+ +
+
+ +**request:** `ProfileUploadRequest` + +
+
+**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + +
+
+
+ +
client.profiles.profiles.get(...) -> ProfileGetResponse
-Returns metadata for every custom (uploaded) FHIR profile on this -instance, across all implementation guides. The full StructureDefinition -JSON is omitted from each entry; fetch a single profile by id to retrieve it. +#### 📝 Description -The `url` query parameter filters by canonical URL. The canonical URL is the -stable key other platform features use to reference a profile (FHIR's -`meta.profile`, `baseDefinition`), since StructureDefinition ids are only -unique within a package. A non-matching filter returns an empty list, not a 404. +
+
+ +
+
+ +Returns a single custom profile by id, including its full StructureDefinition +JSON.
@@ -5475,8 +6371,8 @@ client = PhenomlClient( environment=PhenomlClientEnvironment.DEFAULT, ) -client.profiles.profiles.list( - url="http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", +client.profiles.profiles.get( + id="custom-patient", ) ``` @@ -5493,7 +6389,7 @@ client.profiles.profiles.list(
-**url:** `typing.Optional[str]` — Filter by canonical URL. Accepts the FHIR pinned form `url|version` (split on the last `|`); the bare form matches the current version. +**id:** `str` — The lowercase StructureDefinition id of the custom profile.
@@ -5513,7 +6409,7 @@ client.profiles.profiles.list(
-
client.profiles.profiles.create(...) -> ProfileSummary +
client.profiles.profiles.update(...) -> ProfileSummary
@@ -5525,12 +6421,16 @@ client.profiles.profiles.list(
-Creates a custom profile from a FHIR StructureDefinition supplied as a JSON -object. Metadata such as version, resource type, and url is read from the -StructureDefinition; the lowercase StructureDefinition id becomes the -profile's lookup key. When id is omitted, a random UUID is assigned. Code -system configuration is auto-extracted from the snapshot. Optionally group -the profile under a named implementation guide. +Replaces an existing custom profile with a new StructureDefinition. The +`id` path parameter is authoritative: if the StructureDefinition includes +an `id` it must match the path parameter, and if it omits one the path +parameter is used. The FHIR resource type of the profile cannot change. +When `implementation_guide` is omitted, the profile keeps its existing +implementation guide. A retained version string is allowed only when +re-submitting the profile's current version with an unchanged +StructureDefinition; otherwise it returns a conflict. While the profile +has retained versions, its +canonical URL cannot be changed.
@@ -5554,10 +6454,23 @@ client = PhenomlClient( environment=PhenomlClientEnvironment.DEFAULT, ) -client.profiles.profiles.create( +client.profiles.profiles.update( + id="custom-patient", structure_definition={ - "key": "value" + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": False, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": {"element": [{"id": "Patient", "path": "Patient", "min": 0, "max": "*"}]} }, + implementation_guide="acme-cardiology", ) ``` @@ -5574,6 +6487,14 @@ client.profiles.profiles.create(
+**id:** `str` — The lowercase StructureDefinition id of the custom profile. + +
+
+ +
+
+ **request:** `ProfileUploadRequest`
@@ -5594,7 +6515,7 @@ client.profiles.profiles.create(
-
client.profiles.profiles.get(...) -> ProfileGetResponse +
client.profiles.profiles.delete(...)
@@ -5606,7 +6527,9 @@ client.profiles.profiles.create(
-Returns a single custom profile by id, including its full StructureDefinition JSON. +Permanently deletes a custom profile by id. This also deletes all retained +versions for that profile so the canonical URL can be reused by a later +upload.
@@ -5630,7 +6553,7 @@ client = PhenomlClient( environment=PhenomlClientEnvironment.DEFAULT, ) -client.profiles.profiles.get( +client.profiles.profiles.delete( id="custom-patient", ) @@ -5668,7 +6591,8 @@ client.profiles.profiles.get(
-
client.profiles.profiles.update(...) -> ProfileSummary +## Profiles Versions +
client.profiles.versions.list(...) -> ProfileVersionListResponse
@@ -5680,14 +6604,7 @@ client.profiles.profiles.get(
-Replaces an existing custom profile with a new StructureDefinition. The -`id` path parameter is authoritative: if the StructureDefinition includes -an `id` it must match the path parameter, and if it omits one the path -parameter is used. The FHIR resource type of the profile cannot change. -Code system configuration is -re-derived from the new StructureDefinition. When `implementation_guide` is -omitted, the profile keeps its existing implementation guide. The instance -stores a single version per canonical URL, so this replaces it in place. +Returns retained versions for a custom profile.
@@ -5711,9 +6628,94 @@ client = PhenomlClient( environment=PhenomlClientEnvironment.DEFAULT, ) -client.profiles.profiles.update( +client.profiles.versions.list( id="custom-patient", - structure_definition={ +) + +``` + +
+ + + +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `str` — The lowercase StructureDefinition id of the custom profile. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + + + +
+ +
client.profiles.versions.create(...) -> ProfileSummary +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Adds an immutable StructureDefinition version to a custom profile. If +the profile does not exist, it is created from the submitted version. +The StructureDefinition must include a non-empty `version`; its +canonical URL and resource type must match the profile when one already +exists. If it includes an `id`, that id must match the path parameter; +if it omits `id`, the path parameter is used. Profiles created through +this endpoint are grouped under `custom`. Posting the profile's current +StructureDefinition unchanged retains it as a version. +Version strings may contain letters, numbers, and the punctuation +characters `.`, `_`, `~`, `+`, and `-`; they cannot be exactly `.` or +`..`. Each profile can retain up to 250 versions; delete old +versions before adding more. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.profiles.versions.create( + id="custom-patient", + request={ "key": "value" }, ) @@ -5740,7 +6742,7 @@ client.profiles.profiles.update(
-**request:** `ProfileUploadRequest` +**request:** `ProfileVersionCreateRequest`
@@ -5760,7 +6762,7 @@ client.profiles.profiles.update(
-
client.profiles.profiles.delete(...) +
client.profiles.versions.get(...) -> ProfileGetResponse
@@ -5772,7 +6774,9 @@ client.profiles.profiles.update(
-Permanently deletes a custom profile by id. +Returns metadata and the full StructureDefinition for one retained +version. The returned StructureDefinition's id is the profile id. The +path version is the authored `StructureDefinition.version` value.
@@ -5796,8 +6800,93 @@ client = PhenomlClient( environment=PhenomlClientEnvironment.DEFAULT, ) -client.profiles.profiles.delete( +client.profiles.versions.get( + id="custom-patient", + version="2.0.0", +) + +``` + +
+ + + +#### ⚙️ Parameters + +
+
+ +
+
+ +**id:** `str` — The lowercase StructureDefinition id of the custom profile. + +
+
+ +
+
+ +**version:** `str` — The authored StructureDefinition.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`. + +
+
+ +
+
+ +**request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration. + +
+
+
+
+ + + + +
+ +
client.profiles.versions.delete(...) +
+
+ +#### 📝 Description + +
+
+ +
+
+ +Deletes one retained version from a custom profile. The path +version is the authored `StructureDefinition.version` value. +
+
+
+
+ +#### 🔌 Usage + +
+
+ +
+
+ +```python +from phenoml import PhenomlClient +from phenoml.environment import PhenomlClientEnvironment + +client = PhenomlClient( + client_id="", + client_secret="", + environment=PhenomlClientEnvironment.DEFAULT, +) + +client.profiles.versions.delete( id="custom-patient", + version="2.0.0", ) ``` @@ -5822,6 +6911,14 @@ client.profiles.profiles.delete(
+**version:** `str` — The authored StructureDefinition.version. It may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`. + +
+
+ +
+
+ **request_options:** `typing.Optional[RequestOptions]` — Request-specific configuration.
diff --git a/src/phenoml/__init__.py b/src/phenoml/__init__.py index 73ba5cf..29dc6a0 100644 --- a/src/phenoml/__init__.py +++ b/src/phenoml/__init__.py @@ -16,6 +16,7 @@ fhir_provider, implementation_guides, lang2fhir, + lang2fhir_batch, profiles, summary, tools, @@ -42,6 +43,7 @@ "fhir_provider": ".fhir_provider", "implementation_guides": ".implementation_guides", "lang2fhir": ".lang2fhir", + "lang2fhir_batch": ".lang2fhir_batch", "profiles": ".profiles", "summary": ".summary", "tools": ".tools", @@ -87,6 +89,7 @@ def __dir__(): "fhir_provider", "implementation_guides", "lang2fhir", + "lang2fhir_batch", "profiles", "summary", "tools", diff --git a/src/phenoml/client.py b/src/phenoml/client.py index 4ab4b4f..87cfa7e 100644 --- a/src/phenoml/client.py +++ b/src/phenoml/client.py @@ -22,6 +22,7 @@ from .fhir_provider.client import AsyncFhirProviderClient, FhirProviderClient from .implementation_guides.client import AsyncImplementationGuidesClient, ImplementationGuidesClient from .lang2fhir.client import AsyncLang2FhirClient, Lang2FhirClient + from .lang2fhir_batch.client import AsyncLang2FhirBatchClient, Lang2FhirBatchClient from .profiles.client import AsyncProfilesClient, ProfilesClient from .summary.client import AsyncSummaryClient, SummaryClient from .tools.client import AsyncToolsClient, ToolsClient @@ -218,6 +219,7 @@ def __init__( self._fhir_provider: typing.Optional[FhirProviderClient] = None self._implementation_guides: typing.Optional[ImplementationGuidesClient] = None self._lang2fhir: typing.Optional[Lang2FhirClient] = None + self._lang2fhir_batch: typing.Optional[Lang2FhirBatchClient] = None self._profiles: typing.Optional[ProfilesClient] = None self._summary: typing.Optional[SummaryClient] = None self._tools: typing.Optional[ToolsClient] = None @@ -296,6 +298,14 @@ def lang2fhir(self): self._lang2fhir = Lang2FhirClient(client_wrapper=self._client_wrapper) return self._lang2fhir + @property + def lang2fhir_batch(self): + if self._lang2fhir_batch is None: + from .lang2fhir_batch.client import Lang2FhirBatchClient # noqa: E402 + + self._lang2fhir_batch = Lang2FhirBatchClient(client_wrapper=self._client_wrapper) + return self._lang2fhir_batch + @property def profiles(self): if self._profiles is None: @@ -541,6 +551,7 @@ def __init__( self._fhir_provider: typing.Optional[AsyncFhirProviderClient] = None self._implementation_guides: typing.Optional[AsyncImplementationGuidesClient] = None self._lang2fhir: typing.Optional[AsyncLang2FhirClient] = None + self._lang2fhir_batch: typing.Optional[AsyncLang2FhirBatchClient] = None self._profiles: typing.Optional[AsyncProfilesClient] = None self._summary: typing.Optional[AsyncSummaryClient] = None self._tools: typing.Optional[AsyncToolsClient] = None @@ -619,6 +630,14 @@ def lang2fhir(self): self._lang2fhir = AsyncLang2FhirClient(client_wrapper=self._client_wrapper) return self._lang2fhir + @property + def lang2fhir_batch(self): + if self._lang2fhir_batch is None: + from .lang2fhir_batch.client import AsyncLang2FhirBatchClient # noqa: E402 + + self._lang2fhir_batch = AsyncLang2FhirBatchClient(client_wrapper=self._client_wrapper) + return self._lang2fhir_batch + @property def profiles(self): if self._profiles is None: diff --git a/src/phenoml/core/client_wrapper.py b/src/phenoml/core/client_wrapper.py index bba596d..c03a243 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": "16.11.0", + "X-Fern-SDK-Version": "0.0.0.dev0", **(self.get_custom_headers() or {}), } token = self._get_token() diff --git a/src/phenoml/lang2fhir_batch/__init__.py b/src/phenoml/lang2fhir_batch/__init__.py new file mode 100644 index 0000000..71aa4d2 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/__init__.py @@ -0,0 +1,93 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .types import ( + BatchCounts, + BatchError, + BatchItemStatus, + BatchItemStatusStatus, + BatchJob, + BatchJobStatus, + JobDetailResponse, + JobListResponse, + ResultsPageResponse, + UploadItemResponse, + ) + from .errors import ( + BadRequestError, + ClientClosedRequestError, + ConflictError, + ContentTooLargeError, + GatewayTimeoutError, + InternalServerError, + NotFoundError, + UnauthorizedError, + ) +_dynamic_imports: typing.Dict[str, str] = { + "BadRequestError": ".errors", + "BatchCounts": ".types", + "BatchError": ".types", + "BatchItemStatus": ".types", + "BatchItemStatusStatus": ".types", + "BatchJob": ".types", + "BatchJobStatus": ".types", + "ClientClosedRequestError": ".errors", + "ConflictError": ".errors", + "ContentTooLargeError": ".errors", + "GatewayTimeoutError": ".errors", + "InternalServerError": ".errors", + "JobDetailResponse": ".types", + "JobListResponse": ".types", + "NotFoundError": ".errors", + "ResultsPageResponse": ".types", + "UnauthorizedError": ".errors", + "UploadItemResponse": ".types", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadRequestError", + "BatchCounts", + "BatchError", + "BatchItemStatus", + "BatchItemStatusStatus", + "BatchJob", + "BatchJobStatus", + "ClientClosedRequestError", + "ConflictError", + "ContentTooLargeError", + "GatewayTimeoutError", + "InternalServerError", + "JobDetailResponse", + "JobListResponse", + "NotFoundError", + "ResultsPageResponse", + "UnauthorizedError", + "UploadItemResponse", +] diff --git a/src/phenoml/lang2fhir_batch/client.py b/src/phenoml/lang2fhir_batch/client.py new file mode 100644 index 0000000..2d95deb --- /dev/null +++ b/src/phenoml/lang2fhir_batch/client.py @@ -0,0 +1,898 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from .. import core +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.request_options import RequestOptions +from .raw_client import AsyncRawLang2FhirBatchClient, RawLang2FhirBatchClient +from .types.batch_job import BatchJob +from .types.job_detail_response import JobDetailResponse +from .types.job_list_response import JobListResponse +from .types.results_page_response import ResultsPageResponse +from .types.upload_item_response import UploadItemResponse + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class Lang2FhirBatchClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawLang2FhirBatchClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawLang2FhirBatchClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawLang2FhirBatchClient + """ + return self._raw_client + + def list( + self, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> JobListResponse: + """ + Returns a page of the instance's batch jobs, newest first, without + per-job counts. Jobs are shared across the instance's credentials, so + this lists every batch job on the instance, not just the calling + credential's. + + Parameters + ---------- + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + JobListResponse + A page of batch jobs + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.lang2fhir_batch.list( + cursor="cursor", + limit=1, + ) + """ + _response = self._raw_client.list(cursor=cursor, limit=limit, request_options=request_options) + return _response.data + + def create( + self, *, request_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None + ) -> BatchJob: + """ + Opens an empty batch job. Items arrive on later upload calls and the set + is sealed at finalize. + + Supplying `request_id` makes the create idempotent on that token: a + retried submit whose response was lost returns the original job rather + than opening a second one. This dedupe is scoped to the calling + credential. + + 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. + + Parameters + ---------- + request_id : typing.Optional[str] + Optional client idempotency token. A retried create with the same + token returns the original job instead of opening a second one. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BatchJob + Batch job created (or an idempotent replay of an existing job) + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.lang2fhir_batch.create( + request_id="submit-2025-09-02-batch-001", + ) + """ + _response = self._raw_client.create(request_id=request_id, request_options=request_options) + return _response.data + + def upload_item( + self, + job_id: str, + *, + document: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + create: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + file: typing.Optional[core.File] = OMIT, + request_id: typing.Optional[str] = OMIT, + id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> UploadItemResponse: + """ + Stores one item of a job from a multipart upload. A batch's items arrive + one per request. The item carries **either** a `document` extraction + (whose input file rides as raw bytes in the `file` part) **or** a + `create` extraction (JSON only, no file). + + The upload enforces these rules: + - Set **exactly one** of `document` or `create`. Setting both, or + neither, is a `400`. + - When `document` is set, `file` is **required** — it supplies the + document's binary content (PDF or image). + - When `create` is set, `file` is **forbidden** — a create item carries + no file. + - `document` and `create` must each be a JSON **object**. + + Only the item's structure is checked here: the fields inside `document` + or `create` are not validated at upload. A body that is well-formed JSON + but not a valid request for its endpoint is still accepted with `202` + and fails later during processing, recorded as an item `error`. A + wrong-typed field the endpoint cannot decode fails as `invalid_input`; a + body that decodes but the pipeline rejects (for example, a missing + required field) fails as `processing_failed`. + + Supplying `request_id` makes the upload idempotent on that token. A + re-upload under the same token overwrites the same item rather than + adding a second, so a client that lost an upload's response can safely + re-send it. The response's `deduplicated` is `true` only when the + re-uploaded payload matches the one already stored; a same-token upload + with a changed payload overwrites in place and returns `false`. + + Set a `request_id` on **every** upload: re-sending under the same token + is the only way to repair a lost or incomplete upload, including the one + a finalize `409` reports. Without one, a re-send adds a new item instead + of replacing the missing one, and the job cannot be finalized. + + Uploads are rejected once the job has been finalized (`409`), once it + holds its 500-item limit (`409`), or when the item is too large (`413` — + see the raw-file limit in the API description). + + Parameters + ---------- + job_id : str + + document : typing.Optional[typing.Dict[str, typing.Any]] + The JSON body of `POST /lang2fhir/document/multi`, **without** + its base64 `content` field — the uploaded `file` supplies the + content. Accepts that endpoint's fields (`version`, `provider`, + `patient_reference`, `implementation_guide`, `detection_effort`, + `validation_method`, `config`). This is the **multi**-resource + body: it has no single-`resource` field, and the item's result + is a `DocumentMultiResponse` (a Bundle of resources). Mutually + exclusive with `create`; requires `file`. + + create : typing.Optional[typing.Dict[str, typing.Any]] + The JSON body of `POST /lang2fhir/create/multi`. Accepts that + endpoint's fields (`text`, `version`, `provider`, + `patient_reference`, `implementation_guide`, `detection_effort`, + `validation_method`, `resource_review`). This is the + **multi**-resource body: it has no single-`resource` field, and + the item's result is a `CreateMultiResponse` (a Bundle of + resources). Mutually exclusive with `document`; must **not** be + accompanied by a `file`. + + file : typing.Optional[core.File] + See core.File for more documentation + + request_id : typing.Optional[str] + Optional idempotency token (max 256 bytes). Re-uploading under + the same token overwrites the same item instead of adding a + new one. The token is scoped to this job; the same token in + another job is independent and creates a separate item. + + id : typing.Optional[str] + Optional caller-supplied correlation label (max 512 bytes), + echoed back on status and result listings so you can match the + server's item_id to your own record. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + UploadItemResponse + Item accepted + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.lang2fhir_batch.upload_item( + job_id="job_id", + ) + """ + _response = self._raw_client.upload_item( + job_id, + document=document, + create=create, + file=file, + request_id=request_id, + id=id, + request_options=request_options, + ) + return _response.data + + def finalize(self, job_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> BatchJob: + """ + Seals the job's item set and starts processing. Takes no request body. + Finalize is idempotent: a retried finalize succeeds again. + + If a previous upload did not complete, finalize returns a `409`; re-send + the missing upload (with the same `request_id`), then finalize. + Finalizing a job with no items is a `400`. + + Parameters + ---------- + job_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BatchJob + Job finalized and processing started + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.lang2fhir_batch.finalize( + job_id="job_id", + ) + """ + _response = self._raw_client.finalize(job_id, request_options=request_options) + return _response.data + + def get( + self, + job_id: str, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> JobDetailResponse: + """ + Returns a job's record, its per-status item counts, and one page of + per-item statuses. + + Items are listed in a stable order that is not upload order and is the + same across pages. Match each entry to your own records by its `id` + (your correlation label) or `item_id` (from the upload response), + never by position. + + Parameters + ---------- + job_id : str + + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + JobDetailResponse + The job record, counts, and a page of item statuses + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.lang2fhir_batch.get( + job_id="job_id", + cursor="cursor", + limit=1, + ) + """ + _response = self._raw_client.get(job_id, cursor=cursor, limit=limit, request_options=request_options) + return _response.data + + def get_results( + self, + job_id: str, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ResultsPageResponse: + """ + A lighter status page. Returns the same per-item status entries as + `GET /lang2fhir/batch/{job_id}`, but without the job record or counts, + and the entries carry `result_size` rather than any result content. Use + each entry's `item_id` to fetch that item's result from + `GET /lang2fhir/batch/{job_id}/results/{item_id}`. + + Entries are listed in a stable order that is not upload order and is + the same across pages. Match each entry to your own records by its `id` + (your correlation label) or `item_id` (from the upload response), + never by position. + + Parameters + ---------- + job_id : str + + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ResultsPageResponse + A page of per-item result statuses + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.lang2fhir_batch.get_results( + job_id="job_id", + cursor="cursor", + limit=1, + ) + """ + _response = self._raw_client.get_results(job_id, cursor=cursor, limit=limit, request_options=request_options) + return _response.data + + def get_result( + self, job_id: str, item_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Dict[str, typing.Any]: + """ + Streams one item's stored result bytes verbatim as `application/json`. + The body is the response the item's synchronous multi endpoint would have + returned — a `DocumentMultiResponse` for a document item or a + `CreateMultiResponse` for a create item. + + Only a succeeded item has a result: an item that has not succeeded + (pending, processing, or failed) is a `409`, and a result that has + expired is a `404`. + + Parameters + ---------- + job_id : str + + item_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.Dict[str, typing.Any] + The item's raw result JSON + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.lang2fhir_batch.get_result( + job_id="job_id", + item_id="item_id", + ) + """ + _response = self._raw_client.get_result(job_id, item_id, request_options=request_options) + return _response.data + + +class AsyncLang2FhirBatchClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawLang2FhirBatchClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawLang2FhirBatchClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawLang2FhirBatchClient + """ + return self._raw_client + + async def list( + self, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> JobListResponse: + """ + Returns a page of the instance's batch jobs, newest first, without + per-job counts. Jobs are shared across the instance's credentials, so + this lists every batch job on the instance, not just the calling + credential's. + + Parameters + ---------- + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + JobListResponse + A page of batch jobs + + 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.lang2fhir_batch.list( + cursor="cursor", + limit=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(cursor=cursor, limit=limit, request_options=request_options) + return _response.data + + async def create( + self, *, request_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None + ) -> BatchJob: + """ + Opens an empty batch job. Items arrive on later upload calls and the set + is sealed at finalize. + + Supplying `request_id` makes the create idempotent on that token: a + retried submit whose response was lost returns the original job rather + than opening a second one. This dedupe is scoped to the calling + credential. + + 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. + + Parameters + ---------- + request_id : typing.Optional[str] + Optional client idempotency token. A retried create with the same + token returns the original job instead of opening a second one. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BatchJob + Batch job created (or an idempotent replay of an existing job) + + 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.lang2fhir_batch.create( + request_id="submit-2025-09-02-batch-001", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create(request_id=request_id, request_options=request_options) + return _response.data + + async def upload_item( + self, + job_id: str, + *, + document: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + create: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + file: typing.Optional[core.File] = OMIT, + request_id: typing.Optional[str] = OMIT, + id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> UploadItemResponse: + """ + Stores one item of a job from a multipart upload. A batch's items arrive + one per request. The item carries **either** a `document` extraction + (whose input file rides as raw bytes in the `file` part) **or** a + `create` extraction (JSON only, no file). + + The upload enforces these rules: + - Set **exactly one** of `document` or `create`. Setting both, or + neither, is a `400`. + - When `document` is set, `file` is **required** — it supplies the + document's binary content (PDF or image). + - When `create` is set, `file` is **forbidden** — a create item carries + no file. + - `document` and `create` must each be a JSON **object**. + + Only the item's structure is checked here: the fields inside `document` + or `create` are not validated at upload. A body that is well-formed JSON + but not a valid request for its endpoint is still accepted with `202` + and fails later during processing, recorded as an item `error`. A + wrong-typed field the endpoint cannot decode fails as `invalid_input`; a + body that decodes but the pipeline rejects (for example, a missing + required field) fails as `processing_failed`. + + Supplying `request_id` makes the upload idempotent on that token. A + re-upload under the same token overwrites the same item rather than + adding a second, so a client that lost an upload's response can safely + re-send it. The response's `deduplicated` is `true` only when the + re-uploaded payload matches the one already stored; a same-token upload + with a changed payload overwrites in place and returns `false`. + + Set a `request_id` on **every** upload: re-sending under the same token + is the only way to repair a lost or incomplete upload, including the one + a finalize `409` reports. Without one, a re-send adds a new item instead + of replacing the missing one, and the job cannot be finalized. + + Uploads are rejected once the job has been finalized (`409`), once it + holds its 500-item limit (`409`), or when the item is too large (`413` — + see the raw-file limit in the API description). + + Parameters + ---------- + job_id : str + + document : typing.Optional[typing.Dict[str, typing.Any]] + The JSON body of `POST /lang2fhir/document/multi`, **without** + its base64 `content` field — the uploaded `file` supplies the + content. Accepts that endpoint's fields (`version`, `provider`, + `patient_reference`, `implementation_guide`, `detection_effort`, + `validation_method`, `config`). This is the **multi**-resource + body: it has no single-`resource` field, and the item's result + is a `DocumentMultiResponse` (a Bundle of resources). Mutually + exclusive with `create`; requires `file`. + + create : typing.Optional[typing.Dict[str, typing.Any]] + The JSON body of `POST /lang2fhir/create/multi`. Accepts that + endpoint's fields (`text`, `version`, `provider`, + `patient_reference`, `implementation_guide`, `detection_effort`, + `validation_method`, `resource_review`). This is the + **multi**-resource body: it has no single-`resource` field, and + the item's result is a `CreateMultiResponse` (a Bundle of + resources). Mutually exclusive with `document`; must **not** be + accompanied by a `file`. + + file : typing.Optional[core.File] + See core.File for more documentation + + request_id : typing.Optional[str] + Optional idempotency token (max 256 bytes). Re-uploading under + the same token overwrites the same item instead of adding a + new one. The token is scoped to this job; the same token in + another job is independent and creates a separate item. + + id : typing.Optional[str] + Optional caller-supplied correlation label (max 512 bytes), + echoed back on status and result listings so you can match the + server's item_id to your own record. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + UploadItemResponse + Item accepted + + 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.lang2fhir_batch.upload_item( + job_id="job_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.upload_item( + job_id, + document=document, + create=create, + file=file, + request_id=request_id, + id=id, + request_options=request_options, + ) + return _response.data + + async def finalize(self, job_id: str, *, request_options: typing.Optional[RequestOptions] = None) -> BatchJob: + """ + Seals the job's item set and starts processing. Takes no request body. + Finalize is idempotent: a retried finalize succeeds again. + + If a previous upload did not complete, finalize returns a `409`; re-send + the missing upload (with the same `request_id`), then finalize. + Finalizing a job with no items is a `400`. + + Parameters + ---------- + job_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + BatchJob + Job finalized and processing started + + 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.lang2fhir_batch.finalize( + job_id="job_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.finalize(job_id, request_options=request_options) + return _response.data + + async def get( + self, + job_id: str, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> JobDetailResponse: + """ + Returns a job's record, its per-status item counts, and one page of + per-item statuses. + + Items are listed in a stable order that is not upload order and is the + same across pages. Match each entry to your own records by its `id` + (your correlation label) or `item_id` (from the upload response), + never by position. + + Parameters + ---------- + job_id : str + + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + JobDetailResponse + The job record, counts, and a page of item statuses + + 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.lang2fhir_batch.get( + job_id="job_id", + cursor="cursor", + limit=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(job_id, cursor=cursor, limit=limit, request_options=request_options) + return _response.data + + async def get_results( + self, + job_id: str, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> ResultsPageResponse: + """ + A lighter status page. Returns the same per-item status entries as + `GET /lang2fhir/batch/{job_id}`, but without the job record or counts, + and the entries carry `result_size` rather than any result content. Use + each entry's `item_id` to fetch that item's result from + `GET /lang2fhir/batch/{job_id}/results/{item_id}`. + + Entries are listed in a stable order that is not upload order and is + the same across pages. Match each entry to your own records by its `id` + (your correlation label) or `item_id` (from the upload response), + never by position. + + Parameters + ---------- + job_id : str + + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ResultsPageResponse + A page of per-item result statuses + + 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.lang2fhir_batch.get_results( + job_id="job_id", + cursor="cursor", + limit=1, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_results( + job_id, cursor=cursor, limit=limit, request_options=request_options + ) + return _response.data + + async def get_result( + self, job_id: str, item_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> typing.Dict[str, typing.Any]: + """ + Streams one item's stored result bytes verbatim as `application/json`. + The body is the response the item's synchronous multi endpoint would have + returned — a `DocumentMultiResponse` for a document item or a + `CreateMultiResponse` for a create item. + + Only a succeeded item has a result: an item that has not succeeded + (pending, processing, or failed) is a `409`, and a result that has + expired is a `404`. + + Parameters + ---------- + job_id : str + + item_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + typing.Dict[str, typing.Any] + The item's raw result JSON + + 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.lang2fhir_batch.get_result( + job_id="job_id", + item_id="item_id", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get_result(job_id, item_id, request_options=request_options) + return _response.data diff --git a/src/phenoml/lang2fhir_batch/errors/__init__.py b/src/phenoml/lang2fhir_batch/errors/__init__.py new file mode 100644 index 0000000..00ddd1d --- /dev/null +++ b/src/phenoml/lang2fhir_batch/errors/__init__.py @@ -0,0 +1,59 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .bad_request_error import BadRequestError + from .client_closed_request_error import ClientClosedRequestError + from .conflict_error import ConflictError + from .content_too_large_error import ContentTooLargeError + from .gateway_timeout_error import GatewayTimeoutError + 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", + "ClientClosedRequestError": ".client_closed_request_error", + "ConflictError": ".conflict_error", + "ContentTooLargeError": ".content_too_large_error", + "GatewayTimeoutError": ".gateway_timeout_error", + "InternalServerError": ".internal_server_error", + "NotFoundError": ".not_found_error", + "UnauthorizedError": ".unauthorized_error", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BadRequestError", + "ClientClosedRequestError", + "ConflictError", + "ContentTooLargeError", + "GatewayTimeoutError", + "InternalServerError", + "NotFoundError", + "UnauthorizedError", +] diff --git a/src/phenoml/lang2fhir_batch/errors/bad_request_error.py b/src/phenoml/lang2fhir_batch/errors/bad_request_error.py new file mode 100644 index 0000000..412bae7 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/errors/bad_request_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 BadRequestError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=400, headers=headers, body=body) diff --git a/src/phenoml/lang2fhir_batch/errors/client_closed_request_error.py b/src/phenoml/lang2fhir_batch/errors/client_closed_request_error.py new file mode 100644 index 0000000..fce97ba --- /dev/null +++ b/src/phenoml/lang2fhir_batch/errors/client_closed_request_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 ClientClosedRequestError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=499, headers=headers, body=body) diff --git a/src/phenoml/lang2fhir_batch/errors/conflict_error.py b/src/phenoml/lang2fhir_batch/errors/conflict_error.py new file mode 100644 index 0000000..abe1a9f --- /dev/null +++ b/src/phenoml/lang2fhir_batch/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/lang2fhir_batch/errors/content_too_large_error.py b/src/phenoml/lang2fhir_batch/errors/content_too_large_error.py new file mode 100644 index 0000000..5aa39e3 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/errors/content_too_large_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 ContentTooLargeError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=413, headers=headers, body=body) diff --git a/src/phenoml/lang2fhir_batch/errors/gateway_timeout_error.py b/src/phenoml/lang2fhir_batch/errors/gateway_timeout_error.py new file mode 100644 index 0000000..19acf1d --- /dev/null +++ b/src/phenoml/lang2fhir_batch/errors/gateway_timeout_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 GatewayTimeoutError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=504, headers=headers, body=body) diff --git a/src/phenoml/lang2fhir_batch/errors/internal_server_error.py b/src/phenoml/lang2fhir_batch/errors/internal_server_error.py new file mode 100644 index 0000000..6df0c1d --- /dev/null +++ b/src/phenoml/lang2fhir_batch/errors/internal_server_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 InternalServerError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=500, headers=headers, body=body) diff --git a/src/phenoml/lang2fhir_batch/errors/not_found_error.py b/src/phenoml/lang2fhir_batch/errors/not_found_error.py new file mode 100644 index 0000000..a01562a --- /dev/null +++ b/src/phenoml/lang2fhir_batch/errors/not_found_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 NotFoundError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=404, headers=headers, body=body) diff --git a/src/phenoml/lang2fhir_batch/errors/unauthorized_error.py b/src/phenoml/lang2fhir_batch/errors/unauthorized_error.py new file mode 100644 index 0000000..c990122 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/errors/unauthorized_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 UnauthorizedError(ApiError): + def __init__(self, body: typing.Any, headers: typing.Optional[typing.Dict[str, str]] = None): + super().__init__(status_code=401, headers=headers, body=body) diff --git a/src/phenoml/lang2fhir_batch/raw_client.py b/src/phenoml/lang2fhir_batch/raw_client.py new file mode 100644 index 0000000..2c5dd25 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/raw_client.py @@ -0,0 +1,2001 @@ +# This file was auto-generated by Fern from our API Definition. + +import json +import typing +from json.decoder import JSONDecodeError + +from .. import core +from ..core.api_error import ApiError +from ..core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ..core.http_response import AsyncHttpResponse, HttpResponse +from ..core.jsonable_encoder import encode_path_param, jsonable_encoder +from ..core.parse_error import ParsingError +from ..core.pydantic_utilities import parse_obj_as +from ..core.request_options import RequestOptions +from .errors.bad_request_error import BadRequestError +from .errors.client_closed_request_error import ClientClosedRequestError +from .errors.conflict_error import ConflictError +from .errors.content_too_large_error import ContentTooLargeError +from .errors.gateway_timeout_error import GatewayTimeoutError +from .errors.internal_server_error import InternalServerError +from .errors.not_found_error import NotFoundError +from .errors.unauthorized_error import UnauthorizedError +from .types.batch_job import BatchJob +from .types.job_detail_response import JobDetailResponse +from .types.job_list_response import JobListResponse +from .types.results_page_response import ResultsPageResponse +from .types.upload_item_response import UploadItemResponse +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawLang2FhirBatchClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[JobListResponse]: + """ + Returns a page of the instance's batch jobs, newest first, without + per-job counts. Jobs are shared across the instance's credentials, so + this lists every batch job on the instance, not just the calling + credential's. + + Parameters + ---------- + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[JobListResponse] + A page of batch jobs + """ + _response = self._client_wrapper.httpx_client.request( + "lang2fhir/batch", + method="GET", + params={ + "cursor": cursor, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + JobListResponse, + parse_obj_as( + type_=JobListResponse, # 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 == 401: + raise UnauthorizedError( + 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), + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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 create( + self, *, request_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BatchJob]: + """ + Opens an empty batch job. Items arrive on later upload calls and the set + is sealed at finalize. + + Supplying `request_id` makes the create idempotent on that token: a + retried submit whose response was lost returns the original job rather + than opening a second one. This dedupe is scoped to the calling + credential. + + 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. + + Parameters + ---------- + request_id : typing.Optional[str] + Optional client idempotency token. A retried create with the same + token returns the original job instead of opening a second one. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BatchJob] + Batch job created (or an idempotent replay of an existing job) + """ + _response = self._client_wrapper.httpx_client.request( + "lang2fhir/batch", + method="POST", + json={ + "request_id": request_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BatchJob, + parse_obj_as( + type_=BatchJob, # 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 == 401: + raise UnauthorizedError( + 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(), + ), + ), + ) + if _response.status_code == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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 upload_item( + self, + job_id: str, + *, + document: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + create: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + file: typing.Optional[core.File] = OMIT, + request_id: typing.Optional[str] = OMIT, + id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[UploadItemResponse]: + """ + Stores one item of a job from a multipart upload. A batch's items arrive + one per request. The item carries **either** a `document` extraction + (whose input file rides as raw bytes in the `file` part) **or** a + `create` extraction (JSON only, no file). + + The upload enforces these rules: + - Set **exactly one** of `document` or `create`. Setting both, or + neither, is a `400`. + - When `document` is set, `file` is **required** — it supplies the + document's binary content (PDF or image). + - When `create` is set, `file` is **forbidden** — a create item carries + no file. + - `document` and `create` must each be a JSON **object**. + + Only the item's structure is checked here: the fields inside `document` + or `create` are not validated at upload. A body that is well-formed JSON + but not a valid request for its endpoint is still accepted with `202` + and fails later during processing, recorded as an item `error`. A + wrong-typed field the endpoint cannot decode fails as `invalid_input`; a + body that decodes but the pipeline rejects (for example, a missing + required field) fails as `processing_failed`. + + Supplying `request_id` makes the upload idempotent on that token. A + re-upload under the same token overwrites the same item rather than + adding a second, so a client that lost an upload's response can safely + re-send it. The response's `deduplicated` is `true` only when the + re-uploaded payload matches the one already stored; a same-token upload + with a changed payload overwrites in place and returns `false`. + + Set a `request_id` on **every** upload: re-sending under the same token + is the only way to repair a lost or incomplete upload, including the one + a finalize `409` reports. Without one, a re-send adds a new item instead + of replacing the missing one, and the job cannot be finalized. + + Uploads are rejected once the job has been finalized (`409`), once it + holds its 500-item limit (`409`), or when the item is too large (`413` — + see the raw-file limit in the API description). + + Parameters + ---------- + job_id : str + + document : typing.Optional[typing.Dict[str, typing.Any]] + The JSON body of `POST /lang2fhir/document/multi`, **without** + its base64 `content` field — the uploaded `file` supplies the + content. Accepts that endpoint's fields (`version`, `provider`, + `patient_reference`, `implementation_guide`, `detection_effort`, + `validation_method`, `config`). This is the **multi**-resource + body: it has no single-`resource` field, and the item's result + is a `DocumentMultiResponse` (a Bundle of resources). Mutually + exclusive with `create`; requires `file`. + + create : typing.Optional[typing.Dict[str, typing.Any]] + The JSON body of `POST /lang2fhir/create/multi`. Accepts that + endpoint's fields (`text`, `version`, `provider`, + `patient_reference`, `implementation_guide`, `detection_effort`, + `validation_method`, `resource_review`). This is the + **multi**-resource body: it has no single-`resource` field, and + the item's result is a `CreateMultiResponse` (a Bundle of + resources). Mutually exclusive with `document`; must **not** be + accompanied by a `file`. + + file : typing.Optional[core.File] + See core.File for more documentation + + request_id : typing.Optional[str] + Optional idempotency token (max 256 bytes). Re-uploading under + the same token overwrites the same item instead of adding a + new one. The token is scoped to this job; the same token in + another job is independent and creates a separate item. + + id : typing.Optional[str] + Optional caller-supplied correlation label (max 512 bytes), + echoed back on status and result listings so you can match the + server's item_id to your own record. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[UploadItemResponse] + Item accepted + """ + _response = self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}/items", + method="POST", + data={ + "request_id": request_id, + "id": id, + }, + files={ + **( + {"document": (None, json.dumps(jsonable_encoder(document)), "application/json")} + if document is not OMIT + else {} + ), + **( + {"create": (None, json.dumps(jsonable_encoder(create)), "application/json")} + if create is not OMIT + else {} + ), + **({"file": file} if file is not None else {}), + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + UploadItemResponse, + parse_obj_as( + type_=UploadItemResponse, # 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 == 401: + raise UnauthorizedError( + 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(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + 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), + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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 finalize( + self, job_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[BatchJob]: + """ + Seals the job's item set and starts processing. Takes no request body. + Finalize is idempotent: a retried finalize succeeds again. + + If a previous upload did not complete, finalize returns a `409`; re-send + the missing upload (with the same `request_id`), then finalize. + Finalizing a job with no items is a `400`. + + Parameters + ---------- + job_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[BatchJob] + Job finalized and processing started + """ + _response = self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}/finalize", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BatchJob, + parse_obj_as( + type_=BatchJob, # 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 == 401: + raise UnauthorizedError( + 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(), + ), + ), + ) + if _response.status_code == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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( + self, + job_id: str, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[JobDetailResponse]: + """ + Returns a job's record, its per-status item counts, and one page of + per-item statuses. + + Items are listed in a stable order that is not upload order and is the + same across pages. Match each entry to your own records by its `id` + (your correlation label) or `item_id` (from the upload response), + never by position. + + Parameters + ---------- + job_id : str + + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[JobDetailResponse] + The job record, counts, and a page of item statuses + """ + _response = self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}", + method="GET", + params={ + "cursor": cursor, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + JobDetailResponse, + parse_obj_as( + type_=JobDetailResponse, # 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 == 401: + raise UnauthorizedError( + 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 == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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_results( + self, + job_id: str, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> HttpResponse[ResultsPageResponse]: + """ + A lighter status page. Returns the same per-item status entries as + `GET /lang2fhir/batch/{job_id}`, but without the job record or counts, + and the entries carry `result_size` rather than any result content. Use + each entry's `item_id` to fetch that item's result from + `GET /lang2fhir/batch/{job_id}/results/{item_id}`. + + Entries are listed in a stable order that is not upload order and is + the same across pages. Match each entry to your own records by its `id` + (your correlation label) or `item_id` (from the upload response), + never by position. + + Parameters + ---------- + job_id : str + + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ResultsPageResponse] + A page of per-item result statuses + """ + _response = self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}/results", + method="GET", + params={ + "cursor": cursor, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ResultsPageResponse, + parse_obj_as( + type_=ResultsPageResponse, # 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 == 401: + raise UnauthorizedError( + 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 == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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_result( + self, job_id: str, item_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[typing.Dict[str, typing.Any]]: + """ + Streams one item's stored result bytes verbatim as `application/json`. + The body is the response the item's synchronous multi endpoint would have + returned — a `DocumentMultiResponse` for a document item or a + `CreateMultiResponse` for a create item. + + Only a succeeded item has a result: an item that has not succeeded + (pending, processing, or failed) is a `409`, and a result that has + expired is a `404`. + + Parameters + ---------- + job_id : str + + item_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[typing.Dict[str, typing.Any]] + The item's raw result JSON + """ + _response = self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}/results/{encode_path_param(item_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.Dict[str, typing.Any], + parse_obj_as( + type_=typing.Dict[str, typing.Any], # 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 == 401: + raise UnauthorizedError( + 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(), + ), + ), + ) + if _response.status_code == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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 AsyncRawLang2FhirBatchClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[JobListResponse]: + """ + Returns a page of the instance's batch jobs, newest first, without + per-job counts. Jobs are shared across the instance's credentials, so + this lists every batch job on the instance, not just the calling + credential's. + + Parameters + ---------- + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[JobListResponse] + A page of batch jobs + """ + _response = await self._client_wrapper.httpx_client.request( + "lang2fhir/batch", + method="GET", + params={ + "cursor": cursor, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + JobListResponse, + parse_obj_as( + type_=JobListResponse, # 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 == 401: + raise UnauthorizedError( + 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), + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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 create( + self, *, request_id: typing.Optional[str] = OMIT, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BatchJob]: + """ + Opens an empty batch job. Items arrive on later upload calls and the set + is sealed at finalize. + + Supplying `request_id` makes the create idempotent on that token: a + retried submit whose response was lost returns the original job rather + than opening a second one. This dedupe is scoped to the calling + credential. + + 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. + + Parameters + ---------- + request_id : typing.Optional[str] + Optional client idempotency token. A retried create with the same + token returns the original job instead of opening a second one. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BatchJob] + Batch job created (or an idempotent replay of an existing job) + """ + _response = await self._client_wrapper.httpx_client.request( + "lang2fhir/batch", + method="POST", + json={ + "request_id": request_id, + }, + headers={ + "content-type": "application/json", + }, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BatchJob, + parse_obj_as( + type_=BatchJob, # 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 == 401: + raise UnauthorizedError( + 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(), + ), + ), + ) + if _response.status_code == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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 upload_item( + self, + job_id: str, + *, + document: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + create: typing.Optional[typing.Dict[str, typing.Any]] = OMIT, + file: typing.Optional[core.File] = OMIT, + request_id: typing.Optional[str] = OMIT, + id: typing.Optional[str] = OMIT, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[UploadItemResponse]: + """ + Stores one item of a job from a multipart upload. A batch's items arrive + one per request. The item carries **either** a `document` extraction + (whose input file rides as raw bytes in the `file` part) **or** a + `create` extraction (JSON only, no file). + + The upload enforces these rules: + - Set **exactly one** of `document` or `create`. Setting both, or + neither, is a `400`. + - When `document` is set, `file` is **required** — it supplies the + document's binary content (PDF or image). + - When `create` is set, `file` is **forbidden** — a create item carries + no file. + - `document` and `create` must each be a JSON **object**. + + Only the item's structure is checked here: the fields inside `document` + or `create` are not validated at upload. A body that is well-formed JSON + but not a valid request for its endpoint is still accepted with `202` + and fails later during processing, recorded as an item `error`. A + wrong-typed field the endpoint cannot decode fails as `invalid_input`; a + body that decodes but the pipeline rejects (for example, a missing + required field) fails as `processing_failed`. + + Supplying `request_id` makes the upload idempotent on that token. A + re-upload under the same token overwrites the same item rather than + adding a second, so a client that lost an upload's response can safely + re-send it. The response's `deduplicated` is `true` only when the + re-uploaded payload matches the one already stored; a same-token upload + with a changed payload overwrites in place and returns `false`. + + Set a `request_id` on **every** upload: re-sending under the same token + is the only way to repair a lost or incomplete upload, including the one + a finalize `409` reports. Without one, a re-send adds a new item instead + of replacing the missing one, and the job cannot be finalized. + + Uploads are rejected once the job has been finalized (`409`), once it + holds its 500-item limit (`409`), or when the item is too large (`413` — + see the raw-file limit in the API description). + + Parameters + ---------- + job_id : str + + document : typing.Optional[typing.Dict[str, typing.Any]] + The JSON body of `POST /lang2fhir/document/multi`, **without** + its base64 `content` field — the uploaded `file` supplies the + content. Accepts that endpoint's fields (`version`, `provider`, + `patient_reference`, `implementation_guide`, `detection_effort`, + `validation_method`, `config`). This is the **multi**-resource + body: it has no single-`resource` field, and the item's result + is a `DocumentMultiResponse` (a Bundle of resources). Mutually + exclusive with `create`; requires `file`. + + create : typing.Optional[typing.Dict[str, typing.Any]] + The JSON body of `POST /lang2fhir/create/multi`. Accepts that + endpoint's fields (`text`, `version`, `provider`, + `patient_reference`, `implementation_guide`, `detection_effort`, + `validation_method`, `resource_review`). This is the + **multi**-resource body: it has no single-`resource` field, and + the item's result is a `CreateMultiResponse` (a Bundle of + resources). Mutually exclusive with `document`; must **not** be + accompanied by a `file`. + + file : typing.Optional[core.File] + See core.File for more documentation + + request_id : typing.Optional[str] + Optional idempotency token (max 256 bytes). Re-uploading under + the same token overwrites the same item instead of adding a + new one. The token is scoped to this job; the same token in + another job is independent and creates a separate item. + + id : typing.Optional[str] + Optional caller-supplied correlation label (max 512 bytes), + echoed back on status and result listings so you can match the + server's item_id to your own record. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[UploadItemResponse] + Item accepted + """ + _response = await self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}/items", + method="POST", + data={ + "request_id": request_id, + "id": id, + }, + files={ + **( + {"document": (None, json.dumps(jsonable_encoder(document)), "application/json")} + if document is not OMIT + else {} + ), + **( + {"create": (None, json.dumps(jsonable_encoder(create)), "application/json")} + if create is not OMIT + else {} + ), + **({"file": file} if file is not None else {}), + }, + request_options=request_options, + omit=OMIT, + force_multipart=True, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + UploadItemResponse, + parse_obj_as( + type_=UploadItemResponse, # 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 == 401: + raise UnauthorizedError( + 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(), + ), + ), + ) + if _response.status_code == 413: + raise ContentTooLargeError( + 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), + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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 finalize( + self, job_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[BatchJob]: + """ + Seals the job's item set and starts processing. Takes no request body. + Finalize is idempotent: a retried finalize succeeds again. + + If a previous upload did not complete, finalize returns a `409`; re-send + the missing upload (with the same `request_id`), then finalize. + Finalizing a job with no items is a `400`. + + Parameters + ---------- + job_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[BatchJob] + Job finalized and processing started + """ + _response = await self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}/finalize", + method="POST", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + BatchJob, + parse_obj_as( + type_=BatchJob, # 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 == 401: + raise UnauthorizedError( + 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(), + ), + ), + ) + if _response.status_code == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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( + self, + job_id: str, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[JobDetailResponse]: + """ + Returns a job's record, its per-status item counts, and one page of + per-item statuses. + + Items are listed in a stable order that is not upload order and is the + same across pages. Match each entry to your own records by its `id` + (your correlation label) or `item_id` (from the upload response), + never by position. + + Parameters + ---------- + job_id : str + + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[JobDetailResponse] + The job record, counts, and a page of item statuses + """ + _response = await self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}", + method="GET", + params={ + "cursor": cursor, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + JobDetailResponse, + parse_obj_as( + type_=JobDetailResponse, # 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 == 401: + raise UnauthorizedError( + 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 == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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_results( + self, + job_id: str, + *, + cursor: typing.Optional[str] = None, + limit: typing.Optional[int] = None, + request_options: typing.Optional[RequestOptions] = None, + ) -> AsyncHttpResponse[ResultsPageResponse]: + """ + A lighter status page. Returns the same per-item status entries as + `GET /lang2fhir/batch/{job_id}`, but without the job record or counts, + and the entries carry `result_size` rather than any result content. Use + each entry's `item_id` to fetch that item's result from + `GET /lang2fhir/batch/{job_id}/results/{item_id}`. + + Entries are listed in a stable order that is not upload order and is + the same across pages. Match each entry to your own records by its `id` + (your correlation label) or `item_id` (from the upload response), + never by position. + + Parameters + ---------- + job_id : str + + cursor : typing.Optional[str] + Opaque pagination cursor from a previous page's next_cursor. + + limit : typing.Optional[int] + Page size. Defaults to 20; values above 100 are clamped to 100. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ResultsPageResponse] + A page of per-item result statuses + """ + _response = await self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}/results", + method="GET", + params={ + "cursor": cursor, + "limit": limit, + }, + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ResultsPageResponse, + parse_obj_as( + type_=ResultsPageResponse, # 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 == 401: + raise UnauthorizedError( + 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 == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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_result( + self, job_id: str, item_id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[typing.Dict[str, typing.Any]]: + """ + Streams one item's stored result bytes verbatim as `application/json`. + The body is the response the item's synchronous multi endpoint would have + returned — a `DocumentMultiResponse` for a document item or a + `CreateMultiResponse` for a create item. + + Only a succeeded item has a result: an item that has not succeeded + (pending, processing, or failed) is a `409`, and a result that has + expired is a `404`. + + Parameters + ---------- + job_id : str + + item_id : str + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[typing.Dict[str, typing.Any]] + The item's raw result JSON + """ + _response = await self._client_wrapper.httpx_client.request( + f"lang2fhir/batch/{encode_path_param(job_id)}/results/{encode_path_param(item_id)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + typing.Dict[str, typing.Any], + parse_obj_as( + type_=typing.Dict[str, typing.Any], # 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 == 401: + raise UnauthorizedError( + 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(), + ), + ), + ) + if _response.status_code == 499: + raise ClientClosedRequestError( + 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), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 504: + raise GatewayTimeoutError( + 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/lang2fhir_batch/types/__init__.py b/src/phenoml/lang2fhir_batch/types/__init__.py new file mode 100644 index 0000000..22e6244 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/__init__.py @@ -0,0 +1,65 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + +import typing +from importlib import import_module + +if typing.TYPE_CHECKING: + from .batch_counts import BatchCounts + from .batch_error import BatchError + from .batch_item_status import BatchItemStatus + from .batch_item_status_status import BatchItemStatusStatus + from .batch_job import BatchJob + from .batch_job_status import BatchJobStatus + from .job_detail_response import JobDetailResponse + from .job_list_response import JobListResponse + from .results_page_response import ResultsPageResponse + from .upload_item_response import UploadItemResponse +_dynamic_imports: typing.Dict[str, str] = { + "BatchCounts": ".batch_counts", + "BatchError": ".batch_error", + "BatchItemStatus": ".batch_item_status", + "BatchItemStatusStatus": ".batch_item_status_status", + "BatchJob": ".batch_job", + "BatchJobStatus": ".batch_job_status", + "JobDetailResponse": ".job_detail_response", + "JobListResponse": ".job_list_response", + "ResultsPageResponse": ".results_page_response", + "UploadItemResponse": ".upload_item_response", +} + + +def __getattr__(attr_name: str) -> typing.Any: + module_name = _dynamic_imports.get(attr_name) + if module_name is None: + raise AttributeError(f"No {attr_name} found in _dynamic_imports for module name -> {__name__}") + try: + module = import_module(module_name, __package__) + if module_name == f".{attr_name}": + return module + else: + return getattr(module, attr_name) + except ImportError as e: + raise ImportError(f"Failed to import {attr_name} from {module_name}: {e}") from e + except AttributeError as e: + raise AttributeError(f"Failed to get {attr_name} from {module_name}: {e}") from e + + +def __dir__(): + lazy_attrs = list(_dynamic_imports.keys()) + return sorted(lazy_attrs) + + +__all__ = [ + "BatchCounts", + "BatchError", + "BatchItemStatus", + "BatchItemStatusStatus", + "BatchJob", + "BatchJobStatus", + "JobDetailResponse", + "JobListResponse", + "ResultsPageResponse", + "UploadItemResponse", +] diff --git a/src/phenoml/lang2fhir_batch/types/batch_counts.py b/src/phenoml/lang2fhir_batch/types/batch_counts.py new file mode 100644 index 0000000..867684d --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/batch_counts.py @@ -0,0 +1,27 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class BatchCounts(UniversalBaseModel): + """ + A job's per-status item tally. + """ + + total: int + pending: int + processing: int + succeeded: int + failed: int + + 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/types/batch_error.py b/src/phenoml/lang2fhir_batch/types/batch_error.py new file mode 100644 index 0000000..29efa45 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/batch_error.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel + + +class BatchError(UniversalBaseModel): + """ + A job- or item-level failure. Never contains document or patient content. + """ + + kind: str = pydantic.Field() + """ + 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). + Job-level kinds: `timeout` (the job did not finish within 36 hours + of creation). + """ + + message: str = pydantic.Field() + """ + Human-readable description of the failure. + """ + + 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/types/batch_item_status.py b/src/phenoml/lang2fhir_batch/types/batch_item_status.py new file mode 100644 index 0000000..e2da4e9 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/batch_item_status.py @@ -0,0 +1,64 @@ +# 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 .batch_error import BatchError +from .batch_item_status_status import BatchItemStatusStatus + + +class BatchItemStatus(UniversalBaseModel): + """ + One item's status line. + """ + + item_id: str = pydantic.Field() + """ + Server-assigned opaque item ID, used in result URLs. + """ + + id: typing.Optional[str] = pydantic.Field(default=None) + """ + The caller's correlation label, echoed back; omitted when none was supplied. + """ + + status: BatchItemStatusStatus = pydantic.Field() + """ + Item status. + """ + + attempts: int = pydantic.Field() + """ + Number of processing attempts so far. An item runs up to 3 attempts, + and only an interruption — a worker preemption or a recovered internal + error — is retried; a conversion error fails the item with no retry. + """ + + detect_retries: typing.Optional[int] = pydantic.Field(default=None) + """ + How many times concept detection was re-run for the item because a + pass returned no concepts. Present only on a succeeded item whose + detection re-ran at least once; omitted otherwise. + """ + + result_size: typing.Optional[int] = pydantic.Field(default=None) + """ + Size in bytes of the item's stored result. Present once the item has succeeded. + """ + + error: typing.Optional[BatchError] = None + completed_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) + """ + When the item finished. Absent until then. + """ + + 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/types/batch_item_status_status.py b/src/phenoml/lang2fhir_batch/types/batch_item_status_status.py new file mode 100644 index 0000000..f18891d --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/batch_item_status_status.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +BatchItemStatusStatus = typing.Union[typing.Literal["pending", "processing", "succeeded", "failed"], typing.Any] diff --git a/src/phenoml/lang2fhir_batch/types/batch_job.py b/src/phenoml/lang2fhir_batch/types/batch_job.py new file mode 100644 index 0000000..a5382ab --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/batch_job.py @@ -0,0 +1,73 @@ +# 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 .batch_error import BatchError +from .batch_job_status import BatchJobStatus + + +class BatchJob(UniversalBaseModel): + """ + One batch job record. + """ + + job_id: str = pydantic.Field() + """ + Server-assigned job identifier. + """ + + request_id: typing.Optional[str] = pydantic.Field(default=None) + """ + The idempotency token supplied at create, if any. + """ + + status: BatchJobStatus = pydantic.Field() + """ + Job status. `completed` means every item has finished — some may have + failed, so check `counts` for the split. `failed` is a whole-job + failure (the job could not run at all), distinct from individual item + failures, which never fail the job. + """ + + finalized: bool = pydantic.Field() + """ + Whether the job's item set has been sealed. + """ + + total_items: int = pydantic.Field() + """ + The sealed item count. It is 0 until the job is finalized, so an + upload response always reports 0; poll the job after finalize for the + real count. + """ + + error: typing.Optional[BatchError] = pydantic.Field(default=None) + """ + A whole-job failure. Present only on a `failed` job. + """ + + created_at: dt.datetime + updated_at: dt.datetime + completed_at: typing.Optional[dt.datetime] = pydantic.Field(default=None) + """ + When the job finished. Absent until then. + """ + + expires_at: dt.datetime = pydantic.Field() + """ + When the job and its stored inputs and results are deleted. Set 7 + days out, with the clock restarting when the job reaches `completed` + or `failed`. At expiry the job's `request_id` is freed for reuse. + """ + + 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/types/batch_job_status.py b/src/phenoml/lang2fhir_batch/types/batch_job_status.py new file mode 100644 index 0000000..3f6ffef --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/batch_job_status.py @@ -0,0 +1,5 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +BatchJobStatus = typing.Union[typing.Literal["pending", "processing", "completed", "failed"], typing.Any] diff --git a/src/phenoml/lang2fhir_batch/types/job_detail_response.py b/src/phenoml/lang2fhir_batch/types/job_detail_response.py new file mode 100644 index 0000000..c720eaf --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/job_detail_response.py @@ -0,0 +1,36 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from .batch_counts import BatchCounts +from .batch_item_status import BatchItemStatus +from .batch_job import BatchJob + + +class JobDetailResponse(BatchJob): + """ + A job's record, its counts, and one page of item statuses. + """ + + counts: BatchCounts + items: typing.List[BatchItemStatus] + next_cursor: typing.Optional[str] = pydantic.Field(default=None) + """ + Cursor for the next page, when has_more is true. + """ + + has_more: bool = pydantic.Field() + """ + Whether more item statuses remain beyond this page. + """ + + 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/types/job_list_response.py b/src/phenoml/lang2fhir_batch/types/job_list_response.py new file mode 100644 index 0000000..27042d0 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/job_list_response.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .batch_job import BatchJob + + +class JobListResponse(UniversalBaseModel): + """ + A page of job records, without per-job counts. + """ + + jobs: typing.List[BatchJob] + next_cursor: typing.Optional[str] = pydantic.Field(default=None) + """ + Cursor for the next page, when has_more is true. + """ + + has_more: bool = pydantic.Field() + """ + Whether more jobs remain beyond this page. + """ + + 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/types/results_page_response.py b/src/phenoml/lang2fhir_batch/types/results_page_response.py new file mode 100644 index 0000000..af8aafd --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/results_page_response.py @@ -0,0 +1,33 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .batch_item_status import BatchItemStatus + + +class ResultsPageResponse(UniversalBaseModel): + """ + A page of per-item result statuses, without the job counts. + """ + + results: typing.List[BatchItemStatus] + next_cursor: typing.Optional[str] = pydantic.Field(default=None) + """ + Cursor for the next page, when has_more is true. + """ + + has_more: bool = pydantic.Field() + """ + Whether more results remain beyond this page. + """ + + 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/types/upload_item_response.py b/src/phenoml/lang2fhir_batch/types/upload_item_response.py new file mode 100644 index 0000000..f2fdae8 --- /dev/null +++ b/src/phenoml/lang2fhir_batch/types/upload_item_response.py @@ -0,0 +1,40 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2 +from .batch_job import BatchJob + + +class UploadItemResponse(BatchJob): + """ + The upload response: the job record plus the ID the server assigned the + uploaded item. + """ + + item_id: str = pydantic.Field() + """ + The server-assigned ID of the uploaded item; use it to fetch the result. + """ + + id: typing.Optional[str] = pydantic.Field(default=None) + """ + The caller's correlation label, echoed back; omitted when none was supplied. + """ + + deduplicated: bool = pydantic.Field() + """ + True only when a repeated `request_id` resolved to an item whose + payload matched the one already stored. A same-token upload with a + changed payload overwrites in place and returns false. + """ + + 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/profiles/__init__.py b/src/phenoml/profiles/__init__.py index d7560a6..6975ba2 100644 --- a/src/phenoml/profiles/__init__.py +++ b/src/phenoml/profiles/__init__.py @@ -13,11 +13,21 @@ ProfileSummary, ProfileSummarySource, ProfileUploadRequest, + ProfileVersionCreateRequest, + ProfileVersionListResponse, ) - from .errors import BadRequestError, ForbiddenError, InternalServerError, NotFoundError, UnauthorizedError - from . import profiles + from .errors import ( + BadRequestError, + ConflictError, + ForbiddenError, + InternalServerError, + NotFoundError, + UnauthorizedError, + ) + from . import profiles, versions _dynamic_imports: typing.Dict[str, str] = { "BadRequestError": ".errors", + "ConflictError": ".errors", "FhirResource": ".types", "ForbiddenError": ".errors", "InternalServerError": ".errors", @@ -27,8 +37,11 @@ "ProfileSummary": ".types", "ProfileSummarySource": ".types", "ProfileUploadRequest": ".types", + "ProfileVersionCreateRequest": ".types", + "ProfileVersionListResponse": ".types", "UnauthorizedError": ".errors", "profiles": ".profiles", + "versions": ".versions", } @@ -55,6 +68,7 @@ def __dir__(): __all__ = [ "BadRequestError", + "ConflictError", "FhirResource", "ForbiddenError", "InternalServerError", @@ -64,6 +78,9 @@ def __dir__(): "ProfileSummary", "ProfileSummarySource", "ProfileUploadRequest", + "ProfileVersionCreateRequest", + "ProfileVersionListResponse", "UnauthorizedError", "profiles", + "versions", ] diff --git a/src/phenoml/profiles/client.py b/src/phenoml/profiles/client.py index f78027a..f6c8211 100644 --- a/src/phenoml/profiles/client.py +++ b/src/phenoml/profiles/client.py @@ -10,6 +10,7 @@ if typing.TYPE_CHECKING: from .profiles.client import AsyncProfilesClient as profiles_profiles_client_AsyncProfilesClient from .profiles.client import ProfilesClient as profiles_profiles_client_ProfilesClient + from .versions.client import AsyncVersionsClient, VersionsClient class ProfilesClient: @@ -17,6 +18,7 @@ def __init__(self, *, client_wrapper: SyncClientWrapper): self._raw_client = RawProfilesClient(client_wrapper=client_wrapper) self._client_wrapper = client_wrapper self._profiles: typing.Optional[profiles_profiles_client_ProfilesClient] = None + self._versions: typing.Optional[VersionsClient] = None @property def with_raw_response(self) -> RawProfilesClient: @@ -37,12 +39,21 @@ def profiles(self): self._profiles = profiles_profiles_client_ProfilesClient(client_wrapper=self._client_wrapper) return self._profiles + @property + def versions(self): + if self._versions is None: + from .versions.client import VersionsClient # noqa: E402 + + self._versions = VersionsClient(client_wrapper=self._client_wrapper) + return self._versions + class AsyncProfilesClient: def __init__(self, *, client_wrapper: AsyncClientWrapper): self._raw_client = AsyncRawProfilesClient(client_wrapper=client_wrapper) self._client_wrapper = client_wrapper self._profiles: typing.Optional[profiles_profiles_client_AsyncProfilesClient] = None + self._versions: typing.Optional[AsyncVersionsClient] = None @property def with_raw_response(self) -> AsyncRawProfilesClient: @@ -64,3 +75,11 @@ def profiles(self): self._profiles = profiles_profiles_client_AsyncProfilesClient(client_wrapper=self._client_wrapper) return self._profiles + + @property + def versions(self): + if self._versions is None: + from .versions.client import AsyncVersionsClient # noqa: E402 + + self._versions = AsyncVersionsClient(client_wrapper=self._client_wrapper) + return self._versions diff --git a/src/phenoml/profiles/errors/__init__.py b/src/phenoml/profiles/errors/__init__.py index 306a114..29aa057 100644 --- a/src/phenoml/profiles/errors/__init__.py +++ b/src/phenoml/profiles/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/profiles/errors/conflict_error.py b/src/phenoml/profiles/errors/conflict_error.py new file mode 100644 index 0000000..abe1a9f --- /dev/null +++ b/src/phenoml/profiles/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/profiles/profiles/client.py b/src/phenoml/profiles/profiles/client.py index 5df8c82..c4c0368 100644 --- a/src/phenoml/profiles/profiles/client.py +++ b/src/phenoml/profiles/profiles/client.py @@ -40,12 +40,17 @@ def list( The `url` query parameter filters by canonical URL. The canonical URL is the stable key other platform features use to reference a profile (FHIR's `meta.profile`, `baseDefinition`), since StructureDefinition ids are only - unique within a package. A non-matching filter returns an empty list, not a 404. + unique within a package. An unpinned `url` filter returns metadata for + the profile's current StructureDefinition. Pinned `url|version` filters + resolve a retained version when present; otherwise they can fall back to + the profile's current StructureDefinition, whose content can change + through the profile update endpoint. A non-matching filter returns an + empty list, not a 404. Parameters ---------- url : typing.Optional[str] - Filter by canonical URL. Accepts the FHIR pinned form `url|version` (split on the last `|`); the bare form matches the current version. + Filter by canonical URL. Accepts the FHIR pinned form `url|version`; without a version pin, returns the profile's current StructureDefinition metadata. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -81,9 +86,8 @@ def create( Creates a custom profile from a FHIR StructureDefinition supplied as a JSON object. Metadata such as version, resource type, and url is read from the StructureDefinition; the lowercase StructureDefinition id becomes the - profile's lookup key. When id is omitted, a random UUID is assigned. Code - system configuration is auto-extracted from the snapshot. Optionally group - the profile under a named implementation guide. + profile's lookup key. When id is omitted, a random UUID is assigned. + Optionally group the profile under a named implementation guide. Parameters ---------- @@ -110,7 +114,25 @@ def create( client_secret="YOUR_CLIENT_SECRET", ) client.profiles.profiles.create( - structure_definition={"key": "value"}, + structure_definition={ + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": False, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + {"id": "Patient", "path": "Patient", "min": 0, "max": "*"} + ] + }, + }, + implementation_guide="acme-cardiology", ) """ _response = self._raw_client.create( @@ -122,7 +144,8 @@ def create( def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileGetResponse: """ - Returns a single custom profile by id, including its full StructureDefinition JSON. + Returns a single custom profile by id, including its full StructureDefinition + JSON. Parameters ---------- @@ -165,10 +188,12 @@ def update( `id` path parameter is authoritative: if the StructureDefinition includes an `id` it must match the path parameter, and if it omits one the path parameter is used. The FHIR resource type of the profile cannot change. - Code system configuration is - re-derived from the new StructureDefinition. When `implementation_guide` is - omitted, the profile keeps its existing implementation guide. The instance - stores a single version per canonical URL, so this replaces it in place. + When `implementation_guide` is omitted, the profile keeps its existing + implementation guide. A retained version string is allowed only when + re-submitting the profile's current version with an unchanged + StructureDefinition; otherwise it returns a conflict. While the profile + has retained versions, its + canonical URL cannot be changed. Parameters ---------- @@ -199,7 +224,25 @@ def update( ) client.profiles.profiles.update( id="custom-patient", - structure_definition={"key": "value"}, + structure_definition={ + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": False, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + {"id": "Patient", "path": "Patient", "min": 0, "max": "*"} + ] + }, + }, + implementation_guide="acme-cardiology", ) """ _response = self._raw_client.update( @@ -212,7 +255,9 @@ def update( def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: """ - Permanently deletes a custom profile by id. + Permanently deletes a custom profile by id. This also deletes all retained + versions for that profile so the canonical URL can be reused by a later + upload. Parameters ---------- @@ -268,12 +313,17 @@ async def list( The `url` query parameter filters by canonical URL. The canonical URL is the stable key other platform features use to reference a profile (FHIR's `meta.profile`, `baseDefinition`), since StructureDefinition ids are only - unique within a package. A non-matching filter returns an empty list, not a 404. + unique within a package. An unpinned `url` filter returns metadata for + the profile's current StructureDefinition. Pinned `url|version` filters + resolve a retained version when present; otherwise they can fall back to + the profile's current StructureDefinition, whose content can change + through the profile update endpoint. A non-matching filter returns an + empty list, not a 404. Parameters ---------- url : typing.Optional[str] - Filter by canonical URL. Accepts the FHIR pinned form `url|version` (split on the last `|`); the bare form matches the current version. + Filter by canonical URL. Accepts the FHIR pinned form `url|version`; without a version pin, returns the profile's current StructureDefinition metadata. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -317,9 +367,8 @@ async def create( Creates a custom profile from a FHIR StructureDefinition supplied as a JSON object. Metadata such as version, resource type, and url is read from the StructureDefinition; the lowercase StructureDefinition id becomes the - profile's lookup key. When id is omitted, a random UUID is assigned. Code - system configuration is auto-extracted from the snapshot. Optionally group - the profile under a named implementation guide. + profile's lookup key. When id is omitted, a random UUID is assigned. + Optionally group the profile under a named implementation guide. Parameters ---------- @@ -351,7 +400,25 @@ async def create( async def main() -> None: await client.profiles.profiles.create( - structure_definition={"key": "value"}, + structure_definition={ + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": False, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + {"id": "Patient", "path": "Patient", "min": 0, "max": "*"} + ] + }, + }, + implementation_guide="acme-cardiology", ) @@ -366,7 +433,8 @@ async def main() -> None: async def get(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileGetResponse: """ - Returns a single custom profile by id, including its full StructureDefinition JSON. + Returns a single custom profile by id, including its full StructureDefinition + JSON. Parameters ---------- @@ -417,10 +485,12 @@ async def update( `id` path parameter is authoritative: if the StructureDefinition includes an `id` it must match the path parameter, and if it omits one the path parameter is used. The FHIR resource type of the profile cannot change. - Code system configuration is - re-derived from the new StructureDefinition. When `implementation_guide` is - omitted, the profile keeps its existing implementation guide. The instance - stores a single version per canonical URL, so this replaces it in place. + When `implementation_guide` is omitted, the profile keeps its existing + implementation guide. A retained version string is allowed only when + re-submitting the profile's current version with an unchanged + StructureDefinition; otherwise it returns a conflict. While the profile + has retained versions, its + canonical URL cannot be changed. Parameters ---------- @@ -456,7 +526,25 @@ async def update( async def main() -> None: await client.profiles.profiles.update( id="custom-patient", - structure_definition={"key": "value"}, + structure_definition={ + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": False, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + {"id": "Patient", "path": "Patient", "min": 0, "max": "*"} + ] + }, + }, + implementation_guide="acme-cardiology", ) @@ -472,7 +560,9 @@ async def main() -> None: async def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: """ - Permanently deletes a custom profile by id. + Permanently deletes a custom profile by id. This also deletes all retained + versions for that profile so the canonical URL can be reused by a later + upload. Parameters ---------- diff --git a/src/phenoml/profiles/profiles/raw_client.py b/src/phenoml/profiles/profiles/raw_client.py index c142e36..907ac1d 100644 --- a/src/phenoml/profiles/profiles/raw_client.py +++ b/src/phenoml/profiles/profiles/raw_client.py @@ -11,6 +11,7 @@ from ...core.pydantic_utilities import parse_obj_as from ...core.request_options import RequestOptions 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 @@ -40,12 +41,17 @@ def list( The `url` query parameter filters by canonical URL. The canonical URL is the stable key other platform features use to reference a profile (FHIR's `meta.profile`, `baseDefinition`), since StructureDefinition ids are only - unique within a package. A non-matching filter returns an empty list, not a 404. + unique within a package. An unpinned `url` filter returns metadata for + the profile's current StructureDefinition. Pinned `url|version` filters + resolve a retained version when present; otherwise they can fall back to + the profile's current StructureDefinition, whose content can change + through the profile update endpoint. A non-matching filter returns an + empty list, not a 404. Parameters ---------- url : typing.Optional[str] - Filter by canonical URL. Accepts the FHIR pinned form `url|version` (split on the last `|`); the bare form matches the current version. + Filter by canonical URL. Accepts the FHIR pinned form `url|version`; without a version pin, returns the profile's current StructureDefinition metadata. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -126,9 +132,8 @@ def create( Creates a custom profile from a FHIR StructureDefinition supplied as a JSON object. Metadata such as version, resource type, and url is read from the StructureDefinition; the lowercase StructureDefinition id becomes the - profile's lookup key. When id is omitted, a random UUID is assigned. Code - system configuration is auto-extracted from the snapshot. Optionally group - the profile under a named implementation guide. + profile's lookup key. When id is omitted, a random UUID is assigned. + Optionally group the profile under a named implementation guide. Parameters ---------- @@ -223,7 +228,8 @@ def get( self, id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> HttpResponse[ProfileGetResponse]: """ - Returns a single custom profile by id, including its full StructureDefinition JSON. + Returns a single custom profile by id, including its full StructureDefinition + JSON. Parameters ---------- @@ -330,10 +336,12 @@ def update( `id` path parameter is authoritative: if the StructureDefinition includes an `id` it must match the path parameter, and if it omits one the path parameter is used. The FHIR resource type of the profile cannot change. - Code system configuration is - re-derived from the new StructureDefinition. When `implementation_guide` is - omitted, the profile keeps its existing implementation guide. The instance - stores a single version per canonical URL, so this replaces it in place. + When `implementation_guide` is omitted, the profile keeps its existing + implementation guide. A retained version string is allowed only when + re-submitting the profile's current version with an unchanged + StructureDefinition; otherwise it returns a conflict. While the profile + has retained versions, its + canonical URL cannot be changed. Parameters ---------- @@ -418,6 +426,17 @@ def update( ), ), ) + 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), @@ -440,7 +459,9 @@ def update( def delete(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> HttpResponse[None]: """ - Permanently deletes a custom profile by id. + Permanently deletes a custom profile by id. This also deletes all retained + versions for that profile so the canonical URL can be reused by a later + upload. Parameters ---------- @@ -542,12 +563,17 @@ async def list( The `url` query parameter filters by canonical URL. The canonical URL is the stable key other platform features use to reference a profile (FHIR's `meta.profile`, `baseDefinition`), since StructureDefinition ids are only - unique within a package. A non-matching filter returns an empty list, not a 404. + unique within a package. An unpinned `url` filter returns metadata for + the profile's current StructureDefinition. Pinned `url|version` filters + resolve a retained version when present; otherwise they can fall back to + the profile's current StructureDefinition, whose content can change + through the profile update endpoint. A non-matching filter returns an + empty list, not a 404. Parameters ---------- url : typing.Optional[str] - Filter by canonical URL. Accepts the FHIR pinned form `url|version` (split on the last `|`); the bare form matches the current version. + Filter by canonical URL. Accepts the FHIR pinned form `url|version`; without a version pin, returns the profile's current StructureDefinition metadata. request_options : typing.Optional[RequestOptions] Request-specific configuration. @@ -628,9 +654,8 @@ async def create( Creates a custom profile from a FHIR StructureDefinition supplied as a JSON object. Metadata such as version, resource type, and url is read from the StructureDefinition; the lowercase StructureDefinition id becomes the - profile's lookup key. When id is omitted, a random UUID is assigned. Code - system configuration is auto-extracted from the snapshot. Optionally group - the profile under a named implementation guide. + profile's lookup key. When id is omitted, a random UUID is assigned. + Optionally group the profile under a named implementation guide. Parameters ---------- @@ -725,7 +750,8 @@ async def get( self, id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[ProfileGetResponse]: """ - Returns a single custom profile by id, including its full StructureDefinition JSON. + Returns a single custom profile by id, including its full StructureDefinition + JSON. Parameters ---------- @@ -832,10 +858,12 @@ async def update( `id` path parameter is authoritative: if the StructureDefinition includes an `id` it must match the path parameter, and if it omits one the path parameter is used. The FHIR resource type of the profile cannot change. - Code system configuration is - re-derived from the new StructureDefinition. When `implementation_guide` is - omitted, the profile keeps its existing implementation guide. The instance - stores a single version per canonical URL, so this replaces it in place. + When `implementation_guide` is omitted, the profile keeps its existing + implementation guide. A retained version string is allowed only when + re-submitting the profile's current version with an unchanged + StructureDefinition; otherwise it returns a conflict. While the profile + has retained versions, its + canonical URL cannot be changed. Parameters ---------- @@ -920,6 +948,17 @@ async def update( ), ), ) + 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), @@ -944,7 +983,9 @@ async def delete( self, id: str, *, request_options: typing.Optional[RequestOptions] = None ) -> AsyncHttpResponse[None]: """ - Permanently deletes a custom profile by id. + Permanently deletes a custom profile by id. This also deletes all retained + versions for that profile so the canonical URL can be reused by a later + upload. Parameters ---------- diff --git a/src/phenoml/profiles/types/__init__.py b/src/phenoml/profiles/types/__init__.py index a7a1667..9e00e58 100644 --- a/src/phenoml/profiles/types/__init__.py +++ b/src/phenoml/profiles/types/__init__.py @@ -12,6 +12,8 @@ from .profile_summary import ProfileSummary from .profile_summary_source import ProfileSummarySource from .profile_upload_request import ProfileUploadRequest + from .profile_version_create_request import ProfileVersionCreateRequest + from .profile_version_list_response import ProfileVersionListResponse _dynamic_imports: typing.Dict[str, str] = { "FhirResource": ".fhir_resource", "ProfileGetResponse": ".profile_get_response", @@ -19,6 +21,8 @@ "ProfileSummary": ".profile_summary", "ProfileSummarySource": ".profile_summary_source", "ProfileUploadRequest": ".profile_upload_request", + "ProfileVersionCreateRequest": ".profile_version_create_request", + "ProfileVersionListResponse": ".profile_version_list_response", } @@ -50,4 +54,6 @@ def __dir__(): "ProfileSummary", "ProfileSummarySource", "ProfileUploadRequest", + "ProfileVersionCreateRequest", + "ProfileVersionListResponse", ] diff --git a/src/phenoml/profiles/types/profile_get_response.py b/src/phenoml/profiles/types/profile_get_response.py index b64f0b8..f2ea048 100644 --- a/src/phenoml/profiles/types/profile_get_response.py +++ b/src/phenoml/profiles/types/profile_get_response.py @@ -9,7 +9,11 @@ class ProfileGetResponse(ProfileSummary): - structure_definition: typing.Optional[FhirResource] = pydantic.Field(default=None) + """ + Metadata and full StructureDefinition JSON for either a custom profile's current StructureDefinition or one retained version. + """ + + structure_definition: FhirResource = pydantic.Field() """ The full FHIR StructureDefinition JSON. """ diff --git a/src/phenoml/profiles/types/profile_list_response.py b/src/phenoml/profiles/types/profile_list_response.py index c344c44..dff12c2 100644 --- a/src/phenoml/profiles/types/profile_list_response.py +++ b/src/phenoml/profiles/types/profile_list_response.py @@ -8,7 +8,7 @@ class ProfileListResponse(UniversalBaseModel): - profiles: typing.Optional[typing.List[ProfileSummary]] = None + profiles: typing.List[ProfileSummary] if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/phenoml/profiles/types/profile_summary.py b/src/phenoml/profiles/types/profile_summary.py index 4ddf537..b9687db 100644 --- a/src/phenoml/profiles/types/profile_summary.py +++ b/src/phenoml/profiles/types/profile_summary.py @@ -9,43 +9,65 @@ class ProfileSummary(UniversalBaseModel): - id: typing.Optional[str] = pydantic.Field(default=None) + """ + Metadata for either a custom profile's current StructureDefinition or one retained version. For retained versions, `source` is always `custom` and `updated_at` is equal to `created_at`. + """ + + id: str = pydantic.Field() """ The lowercase StructureDefinition id, used as the profile's lookup key. """ - source: typing.Optional[ProfileSummarySource] = pydantic.Field(default=None) + source: ProfileSummarySource = pydantic.Field() """ - The profile's origin. Listings currently return only custom (uploaded) profiles, so this is always "custom" today; built-in (US Core / R4 base) profiles would be surfaced via an opt-in parameter in a future release, not by changing the default behavior. + The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always "custom" today. """ - resource_type: typing.Optional[str] = pydantic.Field(default=None) + resource_type: str = pydantic.Field() """ The FHIR resource type from the StructureDefinition. """ - url: typing.Optional[str] = pydantic.Field(default=None) + url: str = pydantic.Field() """ The canonical URL from the StructureDefinition. """ - version: typing.Optional[str] = pydantic.Field(default=None) + version: str = pydantic.Field() """ The version from the StructureDefinition.version field. """ - fhir_version: typing.Optional[str] = pydantic.Field(default=None) + status: typing.Optional[str] = pydantic.Field(default=None) + """ + The publication status from StructureDefinition.status. Expected FHIR values include `draft`, `active`, `retired`, and `unknown`; the server preserves authored strings. + """ + + date: typing.Optional[str] = pydantic.Field(default=None) + """ + The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp. + """ + + canonical: str = pydantic.Field() + """ + The canonical profile reference, including the version pin when present. + """ + + fhir_version: str = pydantic.Field() """ The base FHIR version the StructureDefinition targets. """ - implementation_guide: typing.Optional[str] = pydantic.Field(default=None) + implementation_guide: str = pydantic.Field() """ The implementation guide the profile belongs to. """ - created_at: typing.Optional[dt.datetime] = None - updated_at: typing.Optional[dt.datetime] = None + created_at: dt.datetime + updated_at: dt.datetime = pydantic.Field() + """ + Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals `created_at`. + """ if IS_PYDANTIC_V2: model_config: typing.ClassVar[pydantic.ConfigDict] = pydantic.ConfigDict(extra="allow", frozen=True) # type: ignore # Pydantic v2 diff --git a/src/phenoml/profiles/types/profile_version_create_request.py b/src/phenoml/profiles/types/profile_version_create_request.py new file mode 100644 index 0000000..2220af5 --- /dev/null +++ b/src/phenoml/profiles/types/profile_version_create_request.py @@ -0,0 +1,8 @@ +# This file was auto-generated by Fern from our API Definition. + +from .fhir_resource import FhirResource + +ProfileVersionCreateRequest = FhirResource +""" +A FHIR StructureDefinition as a JSON object. Must include resourceType: StructureDefinition, url, type, version, and a snapshot with elements. The canonical URL and resource type must match the profile when one already exists. If id is present, its lowercased value must match the path id; if omitted, the path id is used. The version may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`. +""" diff --git a/src/phenoml/profiles/types/profile_version_list_response.py b/src/phenoml/profiles/types/profile_version_list_response.py new file mode 100644 index 0000000..b68ba51 --- /dev/null +++ b/src/phenoml/profiles/types/profile_version_list_response.py @@ -0,0 +1,20 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +import pydantic +from ...core.pydantic_utilities import IS_PYDANTIC_V2, UniversalBaseModel +from .profile_summary import ProfileSummary + + +class ProfileVersionListResponse(UniversalBaseModel): + versions: typing.List[ProfileSummary] + + 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/profiles/versions/__init__.py b/src/phenoml/profiles/versions/__init__.py new file mode 100644 index 0000000..5cde020 --- /dev/null +++ b/src/phenoml/profiles/versions/__init__.py @@ -0,0 +1,4 @@ +# This file was auto-generated by Fern from our API Definition. + +# isort: skip_file + diff --git a/src/phenoml/profiles/versions/client.py b/src/phenoml/profiles/versions/client.py new file mode 100644 index 0000000..a50fa24 --- /dev/null +++ b/src/phenoml/profiles/versions/client.py @@ -0,0 +1,392 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing + +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.request_options import RequestOptions +from ..types.profile_get_response import ProfileGetResponse +from ..types.profile_summary import ProfileSummary +from ..types.profile_version_create_request import ProfileVersionCreateRequest +from ..types.profile_version_list_response import ProfileVersionListResponse +from .raw_client import AsyncRawVersionsClient, RawVersionsClient + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class VersionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._raw_client = RawVersionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> RawVersionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + RawVersionsClient + """ + return self._raw_client + + def list(self, id: str, *, request_options: typing.Optional[RequestOptions] = None) -> ProfileVersionListResponse: + """ + Returns retained versions for a custom profile. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileVersionListResponse + List of profile versions + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.profiles.versions.list( + id="custom-patient", + ) + """ + _response = self._raw_client.list(id, request_options=request_options) + return _response.data + + def create( + self, id: str, *, request: ProfileVersionCreateRequest, request_options: typing.Optional[RequestOptions] = None + ) -> ProfileSummary: + """ + Adds an immutable StructureDefinition version to a custom profile. If + the profile does not exist, it is created from the submitted version. + The StructureDefinition must include a non-empty `version`; its + canonical URL and resource type must match the profile when one already + exists. If it includes an `id`, that id must match the path parameter; + if it omits `id`, the path parameter is used. Profiles created through + this endpoint are grouped under `custom`. Posting the profile's current + StructureDefinition unchanged retains it as a version. + Version strings may contain letters, numbers, and the punctuation + characters `.`, `_`, `~`, `+`, and `-`; they cannot be exactly `.` or + `..`. Each profile can retain up to 250 versions; delete old + versions before adding more. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + request : ProfileVersionCreateRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileSummary + Profile version successfully created + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.profiles.versions.create( + id="custom-patient", + request={"key": "value"}, + ) + """ + _response = self._raw_client.create(id, request=request, request_options=request_options) + return _response.data + + def get( + self, id: str, version: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ProfileGetResponse: + """ + Returns metadata and the full StructureDefinition for one retained + version. The returned StructureDefinition's id is the profile id. The + path version is the authored `StructureDefinition.version` value. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + version : str + The authored StructureDefinition.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 + ------- + ProfileGetResponse + The requested profile version + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.profiles.versions.get( + id="custom-patient", + version="2.0.0", + ) + """ + _response = self._raw_client.get(id, version, request_options=request_options) + return _response.data + + def delete(self, id: str, version: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Deletes one retained version from a custom profile. The path + version is the authored `StructureDefinition.version` value. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + version : str + The authored StructureDefinition.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 + ------- + None + + Examples + -------- + from phenoml import PhenomlClient + + client = PhenomlClient( + client_id="YOUR_CLIENT_ID", + client_secret="YOUR_CLIENT_SECRET", + ) + client.profiles.versions.delete( + id="custom-patient", + version="2.0.0", + ) + """ + _response = self._raw_client.delete(id, version, request_options=request_options) + return _response.data + + +class AsyncVersionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._raw_client = AsyncRawVersionsClient(client_wrapper=client_wrapper) + + @property + def with_raw_response(self) -> AsyncRawVersionsClient: + """ + Retrieves a raw implementation of this client that returns raw responses. + + Returns + ------- + AsyncRawVersionsClient + """ + return self._raw_client + + async def list( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ProfileVersionListResponse: + """ + Returns retained versions for a custom profile. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileVersionListResponse + List of profile versions + + 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.profiles.versions.list( + id="custom-patient", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.list(id, request_options=request_options) + return _response.data + + async def create( + self, id: str, *, request: ProfileVersionCreateRequest, request_options: typing.Optional[RequestOptions] = None + ) -> ProfileSummary: + """ + Adds an immutable StructureDefinition version to a custom profile. If + the profile does not exist, it is created from the submitted version. + The StructureDefinition must include a non-empty `version`; its + canonical URL and resource type must match the profile when one already + exists. If it includes an `id`, that id must match the path parameter; + if it omits `id`, the path parameter is used. Profiles created through + this endpoint are grouped under `custom`. Posting the profile's current + StructureDefinition unchanged retains it as a version. + Version strings may contain letters, numbers, and the punctuation + characters `.`, `_`, `~`, `+`, and `-`; they cannot be exactly `.` or + `..`. Each profile can retain up to 250 versions; delete old + versions before adding more. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + request : ProfileVersionCreateRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + ProfileSummary + Profile version successfully created + + 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.profiles.versions.create( + id="custom-patient", + request={"key": "value"}, + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.create(id, request=request, request_options=request_options) + return _response.data + + async def get( + self, id: str, version: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> ProfileGetResponse: + """ + Returns metadata and the full StructureDefinition for one retained + version. The returned StructureDefinition's id is the profile id. The + path version is the authored `StructureDefinition.version` value. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + version : str + The authored StructureDefinition.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 + ------- + ProfileGetResponse + The requested profile version + + 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.profiles.versions.get( + id="custom-patient", + version="2.0.0", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.get(id, version, request_options=request_options) + return _response.data + + async def delete(self, id: str, version: str, *, request_options: typing.Optional[RequestOptions] = None) -> None: + """ + Deletes one retained version from a custom profile. The path + version is the authored `StructureDefinition.version` value. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + version : str + The authored StructureDefinition.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 + ------- + None + + 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.profiles.versions.delete( + id="custom-patient", + version="2.0.0", + ) + + + asyncio.run(main()) + """ + _response = await self._raw_client.delete(id, version, request_options=request_options) + return _response.data diff --git a/src/phenoml/profiles/versions/raw_client.py b/src/phenoml/profiles/versions/raw_client.py new file mode 100644 index 0000000..0a1e0c1 --- /dev/null +++ b/src/phenoml/profiles/versions/raw_client.py @@ -0,0 +1,874 @@ +# This file was auto-generated by Fern from our API Definition. + +import typing +from json.decoder import JSONDecodeError + +from ...core.api_error import ApiError +from ...core.client_wrapper import AsyncClientWrapper, SyncClientWrapper +from ...core.http_response import AsyncHttpResponse, HttpResponse +from ...core.jsonable_encoder import encode_path_param +from ...core.parse_error import ParsingError +from ...core.pydantic_utilities import parse_obj_as +from ...core.request_options import RequestOptions +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.profile_get_response import ProfileGetResponse +from ..types.profile_summary import ProfileSummary +from ..types.profile_version_create_request import ProfileVersionCreateRequest +from ..types.profile_version_list_response import ProfileVersionListResponse +from pydantic import ValidationError + +# this is used as the default value for optional parameters +OMIT = typing.cast(typing.Any, ...) + + +class RawVersionsClient: + def __init__(self, *, client_wrapper: SyncClientWrapper): + self._client_wrapper = client_wrapper + + def list( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ProfileVersionListResponse]: + """ + Returns retained versions for a custom profile. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileVersionListResponse] + List of profile versions + """ + _response = self._client_wrapper.httpx_client.request( + f"fhir/profiles/{encode_path_param(id)}/versions", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileVersionListResponse, + parse_obj_as( + type_=ProfileVersionListResponse, # 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 == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + 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 == 500: + raise InternalServerError( + 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 create( + self, id: str, *, request: ProfileVersionCreateRequest, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ProfileSummary]: + """ + Adds an immutable StructureDefinition version to a custom profile. If + the profile does not exist, it is created from the submitted version. + The StructureDefinition must include a non-empty `version`; its + canonical URL and resource type must match the profile when one already + exists. If it includes an `id`, that id must match the path parameter; + if it omits `id`, the path parameter is used. Profiles created through + this endpoint are grouped under `custom`. Posting the profile's current + StructureDefinition unchanged retains it as a version. + Version strings may contain letters, numbers, and the punctuation + characters `.`, `_`, `~`, `+`, and `-`; they cannot be exactly `.` or + `..`. Each profile can retain up to 250 versions; delete old + versions before adding more. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + request : ProfileVersionCreateRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + HttpResponse[ProfileSummary] + Profile version successfully created + """ + _response = self._client_wrapper.httpx_client.request( + f"fhir/profiles/{encode_path_param(id)}/versions", + method="POST", + json=request, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileSummary, + parse_obj_as( + type_=ProfileSummary, # 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 == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + 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(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + 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( + self, id: str, version: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[ProfileGetResponse]: + """ + Returns metadata and the full StructureDefinition for one retained + version. The returned StructureDefinition's id is the profile id. The + path version is the authored `StructureDefinition.version` value. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + version : str + The authored StructureDefinition.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[ProfileGetResponse] + The requested profile version + """ + _response = self._client_wrapper.httpx_client.request( + f"fhir/profiles/{encode_path_param(id)}/versions/{encode_path_param(version)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileGetResponse, + parse_obj_as( + type_=ProfileGetResponse, # 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 == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + 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 == 500: + raise InternalServerError( + 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 delete( + self, id: str, version: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> HttpResponse[None]: + """ + Deletes one retained version from a custom profile. The path + version is the authored `StructureDefinition.version` value. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + version : str + The authored StructureDefinition.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[None] + """ + _response = self._client_wrapper.httpx_client.request( + f"fhir/profiles/{encode_path_param(id)}/versions/{encode_path_param(version)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return HttpResponse(response=_response, data=None) + 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 == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + 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 == 500: + raise InternalServerError( + 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 AsyncRawVersionsClient: + def __init__(self, *, client_wrapper: AsyncClientWrapper): + self._client_wrapper = client_wrapper + + async def list( + self, id: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ProfileVersionListResponse]: + """ + Returns retained versions for a custom profile. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileVersionListResponse] + List of profile versions + """ + _response = await self._client_wrapper.httpx_client.request( + f"fhir/profiles/{encode_path_param(id)}/versions", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileVersionListResponse, + parse_obj_as( + type_=ProfileVersionListResponse, # 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 == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + 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 == 500: + raise InternalServerError( + 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 create( + self, id: str, *, request: ProfileVersionCreateRequest, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ProfileSummary]: + """ + Adds an immutable StructureDefinition version to a custom profile. If + the profile does not exist, it is created from the submitted version. + The StructureDefinition must include a non-empty `version`; its + canonical URL and resource type must match the profile when one already + exists. If it includes an `id`, that id must match the path parameter; + if it omits `id`, the path parameter is used. Profiles created through + this endpoint are grouped under `custom`. Posting the profile's current + StructureDefinition unchanged retains it as a version. + Version strings may contain letters, numbers, and the punctuation + characters `.`, `_`, `~`, `+`, and `-`; they cannot be exactly `.` or + `..`. Each profile can retain up to 250 versions; delete old + versions before adding more. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + request : ProfileVersionCreateRequest + + request_options : typing.Optional[RequestOptions] + Request-specific configuration. + + Returns + ------- + AsyncHttpResponse[ProfileSummary] + Profile version successfully created + """ + _response = await self._client_wrapper.httpx_client.request( + f"fhir/profiles/{encode_path_param(id)}/versions", + method="POST", + json=request, + request_options=request_options, + omit=OMIT, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileSummary, + parse_obj_as( + type_=ProfileSummary, # 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 == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + 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(), + ), + ), + ) + if _response.status_code == 500: + raise InternalServerError( + 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( + self, id: str, version: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[ProfileGetResponse]: + """ + Returns metadata and the full StructureDefinition for one retained + version. The returned StructureDefinition's id is the profile id. The + path version is the authored `StructureDefinition.version` value. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + version : str + The authored StructureDefinition.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[ProfileGetResponse] + The requested profile version + """ + _response = await self._client_wrapper.httpx_client.request( + f"fhir/profiles/{encode_path_param(id)}/versions/{encode_path_param(version)}", + method="GET", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + _data = typing.cast( + ProfileGetResponse, + parse_obj_as( + type_=ProfileGetResponse, # 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 == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + 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 == 500: + raise InternalServerError( + 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 delete( + self, id: str, version: str, *, request_options: typing.Optional[RequestOptions] = None + ) -> AsyncHttpResponse[None]: + """ + Deletes one retained version from a custom profile. The path + version is the authored `StructureDefinition.version` value. + + Parameters + ---------- + id : str + The lowercase StructureDefinition id of the custom profile. + + version : str + The authored StructureDefinition.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[None] + """ + _response = await self._client_wrapper.httpx_client.request( + f"fhir/profiles/{encode_path_param(id)}/versions/{encode_path_param(version)}", + method="DELETE", + request_options=request_options, + ) + try: + if 200 <= _response.status_code < 300: + return AsyncHttpResponse(response=_response, data=None) + 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 == 401: + raise UnauthorizedError( + headers=dict(_response.headers), + body=typing.cast( + typing.Any, + parse_obj_as( + type_=typing.Any, # type: ignore + object_=_response.json(), + ), + ), + ) + if _response.status_code == 403: + raise ForbiddenError( + 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 == 500: + raise InternalServerError( + 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/tests/wire/test_lang2FhirBatch.py b/tests/wire/test_lang2FhirBatch.py new file mode 100644 index 0000000..58179d3 --- /dev/null +++ b/tests/wire/test_lang2FhirBatch.py @@ -0,0 +1,78 @@ +from .conftest import get_client, verify_request_count + + +def test_lang2FhirBatch_list_() -> None: + """Test list endpoint with WireMock""" + test_id = "lang2fhir_batch.list_.0" + client = get_client(test_id) + client.lang2fhir_batch.list( + cursor="cursor", + limit=1, + ) + verify_request_count(test_id, "GET", "/lang2fhir/batch", {"cursor": "cursor", "limit": "1"}, 1) + + +def test_lang2FhirBatch_create() -> None: + """Test create endpoint with WireMock""" + test_id = "lang2fhir_batch.create.0" + client = get_client(test_id) + client.lang2fhir_batch.create( + request_id="submit-2025-09-02-batch-001", + ) + verify_request_count(test_id, "POST", "/lang2fhir/batch", None, 1) + + +def test_lang2FhirBatch_upload_item() -> None: + """Test uploadItem endpoint with WireMock""" + test_id = "lang2fhir_batch.upload_item.0" + client = get_client(test_id) + client.lang2fhir_batch.upload_item( + job_id="job_id", + file="example_file", + ) + verify_request_count(test_id, "POST", "/lang2fhir/batch/job_id/items", None, 1) + + +def test_lang2FhirBatch_finalize() -> None: + """Test finalize endpoint with WireMock""" + test_id = "lang2fhir_batch.finalize.0" + client = get_client(test_id) + client.lang2fhir_batch.finalize( + job_id="job_id", + ) + verify_request_count(test_id, "POST", "/lang2fhir/batch/job_id/finalize", None, 1) + + +def test_lang2FhirBatch_get() -> None: + """Test get endpoint with WireMock""" + test_id = "lang2fhir_batch.get.0" + client = get_client(test_id) + client.lang2fhir_batch.get( + job_id="job_id", + cursor="cursor", + limit=1, + ) + verify_request_count(test_id, "GET", "/lang2fhir/batch/job_id", {"cursor": "cursor", "limit": "1"}, 1) + + +def test_lang2FhirBatch_get_results() -> None: + """Test getResults endpoint with WireMock""" + test_id = "lang2fhir_batch.get_results.0" + client = get_client(test_id) + client.lang2fhir_batch.get_results( + job_id="job_id", + cursor="cursor", + limit=1, + ) + verify_request_count(test_id, "GET", "/lang2fhir/batch/job_id/results", {"cursor": "cursor", "limit": "1"}, 1) + + +def test_lang2FhirBatch_get_result() -> None: + """Test getResult endpoint with WireMock""" + test_id = "lang2fhir_batch.get_result.0" + client = get_client(test_id) + client.lang2fhir_batch.get_result( + job_id="job_id", + item_id="item_id", + ) + verify_request_count(test_id, "GET", "/lang2fhir/batch/job_id/results/item_id", None, 1) diff --git a/tests/wire/test_profiles_profiles.py b/tests/wire/test_profiles_profiles.py index 991135d..bf9f349 100644 --- a/tests/wire/test_profiles_profiles.py +++ b/tests/wire/test_profiles_profiles.py @@ -18,7 +18,21 @@ def test_profiles_profiles_create() -> None: test_id = "profiles.profiles.create.0" client = get_client(test_id) client.profiles.profiles.create( - structure_definition={"key": "value"}, + structure_definition={ + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": False, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": {"element": [{"id": "Patient", "path": "Patient", "min": 0, "max": "*"}]}, + }, + implementation_guide="acme-cardiology", ) verify_request_count(test_id, "POST", "/fhir/profiles", None, 1) @@ -39,7 +53,21 @@ def test_profiles_profiles_update() -> None: client = get_client(test_id) client.profiles.profiles.update( id="custom-patient", - structure_definition={"key": "value"}, + structure_definition={ + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": False, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": {"element": [{"id": "Patient", "path": "Patient", "min": 0, "max": "*"}]}, + }, + implementation_guide="acme-cardiology", ) verify_request_count(test_id, "PUT", "/fhir/profiles/custom-patient", None, 1) diff --git a/tests/wire/test_profiles_versions.py b/tests/wire/test_profiles_versions.py new file mode 100644 index 0000000..e530f3e --- /dev/null +++ b/tests/wire/test_profiles_versions.py @@ -0,0 +1,44 @@ +from .conftest import get_client, verify_request_count + + +def test_profiles_versions_list_() -> None: + """Test list endpoint with WireMock""" + test_id = "profiles.versions.list_.0" + client = get_client(test_id) + client.profiles.versions.list( + id="custom-patient", + ) + verify_request_count(test_id, "GET", "/fhir/profiles/custom-patient/versions", None, 1) + + +def test_profiles_versions_create() -> None: + """Test create endpoint with WireMock""" + test_id = "profiles.versions.create.0" + client = get_client(test_id) + client.profiles.versions.create( + id="custom-patient", + request={"key": "value"}, + ) + verify_request_count(test_id, "POST", "/fhir/profiles/custom-patient/versions", None, 1) + + +def test_profiles_versions_get() -> None: + """Test get endpoint with WireMock""" + test_id = "profiles.versions.get.0" + client = get_client(test_id) + client.profiles.versions.get( + id="custom-patient", + version="2.0.0", + ) + verify_request_count(test_id, "GET", "/fhir/profiles/custom-patient/versions/2.0.0", None, 1) + + +def test_profiles_versions_delete() -> None: + """Test delete endpoint with WireMock""" + test_id = "profiles.versions.delete.0" + client = get_client(test_id) + client.profiles.versions.delete( + id="custom-patient", + version="2.0.0", + ) + verify_request_count(test_id, "DELETE", "/fhir/profiles/custom-patient/versions/2.0.0", None, 1) diff --git a/wiremock/wiremock-mappings.json b/wiremock/wiremock-mappings.json index 11ab612..042a584 100644 --- a/wiremock/wiremock-mappings.json +++ b/wiremock/wiremock-mappings.json @@ -1645,6 +1645,241 @@ } } }, + { + "id": "5f010e1c-ed6c-4a4c-a052-a29ce2aadff7", + "name": "List batch jobs - default", + "request": { + "urlPathTemplate": "/lang2fhir/batch", + "method": "GET", + "queryParameters": { + "cursor": { + "equalTo": "cursor" + }, + "limit": { + "equalTo": "1" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"jobs\": [\n {\n \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n \"request_id\": \"submit-2025-09-02-batch-001\",\n \"status\": \"pending\",\n \"finalized\": false,\n \"total_items\": 12,\n \"error\": {\n \"kind\": \"processing_failed\",\n \"message\": \"the item could not be converted\"\n },\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\",\n \"completed_at\": \"2024-01-15T09:30:00Z\",\n \"expires_at\": \"2024-01-15T09:30:00Z\"\n }\n ],\n \"next_cursor\": \"next_cursor\",\n \"has_more\": false\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "5f010e1c-ed6c-4a4c-a052-a29ce2aadff7", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + }, + "postServeActions": [] + }, + { + "id": "bde73df9-796c-44ff-98c8-45bd5f3f393d", + "name": "Create a batch job - default", + "request": { + "urlPathTemplate": "/lang2fhir/batch", + "method": "POST" + }, + "response": { + "status": 202, + "body": "{\n \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n \"request_id\": \"submit-2025-09-02-batch-001\",\n \"status\": \"pending\",\n \"finalized\": false,\n \"total_items\": 12,\n \"error\": {\n \"kind\": \"processing_failed\",\n \"message\": \"the item could not be converted\"\n },\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\",\n \"completed_at\": \"2024-01-15T09:30:00Z\",\n \"expires_at\": \"2024-01-15T09:30:00Z\"\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "bde73df9-796c-44ff-98c8-45bd5f3f393d", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, + { + "id": "7e1803b1-313b-4543-b327-fdac0c385aba", + "name": "Upload one batch item - default", + "request": { + "urlPathTemplate": "/lang2fhir/batch/{job_id}/items", + "method": "POST", + "pathParameters": { + "job_id": { + "equalTo": "job_id" + } + } + }, + "response": { + "status": 202, + "body": "{\n \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n \"request_id\": \"submit-2025-09-02-batch-001\",\n \"status\": \"pending\",\n \"finalized\": false,\n \"total_items\": 12,\n \"error\": {\n \"kind\": \"processing_failed\",\n \"message\": \"the item could not be converted\"\n },\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\",\n \"completed_at\": \"2024-01-15T09:30:00Z\",\n \"expires_at\": \"2024-01-15T09:30:00Z\",\n \"item_id\": \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\n \"id\": \"chart-note-0042\",\n \"deduplicated\": false\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "7e1803b1-313b-4543-b327-fdac0c385aba", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, + { + "id": "9a02971f-22a4-445b-84b8-736ae70fe586", + "name": "Finalize a batch job - default", + "request": { + "urlPathTemplate": "/lang2fhir/batch/{job_id}/finalize", + "method": "POST", + "pathParameters": { + "job_id": { + "equalTo": "job_id" + } + } + }, + "response": { + "status": 202, + "body": "{\n \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n \"request_id\": \"submit-2025-09-02-batch-001\",\n \"status\": \"pending\",\n \"finalized\": false,\n \"total_items\": 12,\n \"error\": {\n \"kind\": \"processing_failed\",\n \"message\": \"the item could not be converted\"\n },\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\",\n \"completed_at\": \"2024-01-15T09:30:00Z\",\n \"expires_at\": \"2024-01-15T09:30:00Z\"\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "9a02971f-22a4-445b-84b8-736ae70fe586", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, + { + "id": "4ae6e86d-67a4-4b81-afb6-652f0a1d0a26", + "name": "Get a batch job - default", + "request": { + "urlPathTemplate": "/lang2fhir/batch/{job_id}", + "method": "GET", + "pathParameters": { + "job_id": { + "equalTo": "job_id" + } + }, + "queryParameters": { + "cursor": { + "equalTo": "cursor" + }, + "limit": { + "equalTo": "1" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"job_id\": \"l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d\",\n \"request_id\": \"submit-2025-09-02-batch-001\",\n \"status\": \"pending\",\n \"finalized\": false,\n \"total_items\": 12,\n \"error\": {\n \"kind\": \"processing_failed\",\n \"message\": \"the item could not be converted\"\n },\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\",\n \"completed_at\": \"2024-01-15T09:30:00Z\",\n \"expires_at\": \"2024-01-15T09:30:00Z\",\n \"counts\": {\n \"total\": 12,\n \"pending\": 3,\n \"processing\": 2,\n \"succeeded\": 6,\n \"failed\": 1\n },\n \"items\": [\n {\n \"item_id\": \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\n \"id\": \"chart-note-0042\",\n \"status\": \"pending\",\n \"attempts\": 1,\n \"detect_retries\": 1,\n \"result_size\": 20482,\n \"error\": {\n \"kind\": \"processing_failed\",\n \"message\": \"the item could not be converted\"\n },\n \"completed_at\": \"2024-01-15T09:30:00Z\"\n }\n ],\n \"next_cursor\": \"next_cursor\",\n \"has_more\": false\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "4ae6e86d-67a4-4b81-afb6-652f0a1d0a26", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, + { + "id": "f2086f8a-8680-4f4b-981e-69c8d9245f0f", + "name": "List a batch job's results - default", + "request": { + "urlPathTemplate": "/lang2fhir/batch/{job_id}/results", + "method": "GET", + "pathParameters": { + "job_id": { + "equalTo": "job_id" + } + }, + "queryParameters": { + "cursor": { + "equalTo": "cursor" + }, + "limit": { + "equalTo": "1" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"results\": [\n {\n \"item_id\": \"9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08\",\n \"id\": \"chart-note-0042\",\n \"status\": \"pending\",\n \"attempts\": 1,\n \"detect_retries\": 1,\n \"result_size\": 20482,\n \"error\": {\n \"kind\": \"processing_failed\",\n \"message\": \"the item could not be converted\"\n },\n \"completed_at\": \"2024-01-15T09:30:00Z\"\n }\n ],\n \"next_cursor\": \"next_cursor\",\n \"has_more\": false\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "f2086f8a-8680-4f4b-981e-69c8d9245f0f", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, + { + "id": "29496381-fe05-4535-a70b-b830e4825e6b", + "name": "Get one batch item's result - default", + "request": { + "urlPathTemplate": "/lang2fhir/batch/{job_id}/results/{item_id}", + "method": "GET", + "pathParameters": { + "job_id": { + "equalTo": "job_id" + }, + "item_id": { + "equalTo": "item_id" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"key\": \"value\"\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "29496381-fe05-4535-a70b-b830e4825e6b", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, { "id": "9343ec90-6074-4728-ba0b-3e777524c972", "name": "List custom FHIR profiles - default", @@ -1659,7 +1894,7 @@ }, "response": { "status": 200, - "body": "{\n \"profiles\": [\n {\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n }\n ]\n}", + "body": "{\n \"profiles\": [\n {\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"status\": \"active\",\n \"date\": \"2026-08-24\",\n \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2026-08-24T15:04:05Z\",\n \"updated_at\": \"2026-08-25T16:04:05Z\"\n }\n ]\n}", "headers": { "Content-Type": "application/json" } @@ -1686,7 +1921,7 @@ }, "response": { "status": 201, - "body": "{\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n}", + "body": "{\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"status\": \"active\",\n \"date\": \"2026-08-24\",\n \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2026-08-24T15:04:05Z\",\n \"updated_at\": \"2026-08-25T16:04:05Z\"\n}", "headers": { "Content-Type": "application/json" } @@ -1717,7 +1952,7 @@ }, "response": { "status": 200, - "body": "{\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\",\n \"structure_definition\": {\n \"key\": \"value\"\n }\n}", + "body": "{\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"status\": \"active\",\n \"date\": \"2026-08-24\",\n \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2026-08-24T15:04:05Z\",\n \"updated_at\": \"2026-08-25T16:04:05Z\",\n \"structure_definition\": {\n \"resourceType\": \"StructureDefinition\",\n \"id\": \"custom-patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"name\": \"CustomPatient\",\n \"status\": \"active\",\n \"fhirVersion\": \"4.0.1\",\n \"kind\": \"resource\",\n \"abstract\": false,\n \"type\": \"Patient\",\n \"baseDefinition\": \"http://hl7.org/fhir/StructureDefinition/Patient\",\n \"derivation\": \"constraint\",\n \"snapshot\": {\n \"element\": [\n {\n \"id\": \"Patient\",\n \"path\": \"Patient\",\n \"min\": 0,\n \"max\": \"*\"\n }\n ]\n }\n }\n}", "headers": { "Content-Type": "application/json" } @@ -1748,7 +1983,7 @@ }, "response": { "status": 200, - "body": "{\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2024-01-15T09:30:00Z\",\n \"updated_at\": \"2024-01-15T09:30:00Z\"\n}", + "body": "{\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"status\": \"active\",\n \"date\": \"2026-08-24\",\n \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2026-08-24T15:04:05Z\",\n \"updated_at\": \"2026-08-25T16:04:05Z\"\n}", "headers": { "Content-Type": "application/json" } @@ -1796,6 +2031,136 @@ } } }, + { + "id": "ece33377-1b58-43bf-9eb4-15018a2aa8a3", + "name": "List versions for a custom FHIR profile - default", + "request": { + "urlPathTemplate": "/fhir/profiles/{id}/versions", + "method": "GET", + "pathParameters": { + "id": { + "equalTo": "custom-patient" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"versions\": [\n {\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"2.0.0\",\n \"status\": \"active\",\n \"date\": \"2026-08-26\",\n \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|2.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2026-08-26T15:04:05Z\",\n \"updated_at\": \"2026-08-26T15:04:05Z\"\n },\n {\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"status\": \"active\",\n \"date\": \"2026-08-24\",\n \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2026-08-24T15:04:05Z\",\n \"updated_at\": \"2026-08-24T15:04:05Z\"\n }\n ]\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "ece33377-1b58-43bf-9eb4-15018a2aa8a3", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, + { + "id": "75dac82d-0cf5-471e-9221-a2bc87f07808", + "name": "Add a retained version to a custom FHIR profile - default", + "request": { + "urlPathTemplate": "/fhir/profiles/{id}/versions", + "method": "POST", + "pathParameters": { + "id": { + "equalTo": "custom-patient" + } + } + }, + "response": { + "status": 201, + "body": "{\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"status\": \"active\",\n \"date\": \"2026-08-24\",\n \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2026-08-24T15:04:05Z\",\n \"updated_at\": \"2026-08-25T16:04:05Z\"\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "75dac82d-0cf5-471e-9221-a2bc87f07808", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, + { + "id": "f7707266-d3fb-4351-a61c-53398a9631a3", + "name": "Get a retained custom FHIR profile version - default", + "request": { + "urlPathTemplate": "/fhir/profiles/{id}/versions/{version}", + "method": "GET", + "pathParameters": { + "id": { + "equalTo": "custom-patient" + }, + "version": { + "equalTo": "2.0.0" + } + } + }, + "response": { + "status": 200, + "body": "{\n \"id\": \"custom-patient\",\n \"source\": \"custom\",\n \"resource_type\": \"Patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"status\": \"active\",\n \"date\": \"2026-08-24\",\n \"canonical\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0\",\n \"fhir_version\": \"4.0.1\",\n \"implementation_guide\": \"acme-cardiology\",\n \"created_at\": \"2026-08-24T15:04:05Z\",\n \"updated_at\": \"2026-08-25T16:04:05Z\",\n \"structure_definition\": {\n \"resourceType\": \"StructureDefinition\",\n \"id\": \"custom-patient\",\n \"url\": \"http://phenoml.com/fhir/StructureDefinition/custom-patient\",\n \"version\": \"1.0.0\",\n \"name\": \"CustomPatient\",\n \"status\": \"active\",\n \"fhirVersion\": \"4.0.1\",\n \"kind\": \"resource\",\n \"abstract\": false,\n \"type\": \"Patient\",\n \"baseDefinition\": \"http://hl7.org/fhir/StructureDefinition/Patient\",\n \"derivation\": \"constraint\",\n \"snapshot\": {\n \"element\": [\n {\n \"id\": \"Patient\",\n \"path\": \"Patient\",\n \"min\": 0,\n \"max\": \"*\"\n }\n ]\n }\n }\n}", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "f7707266-d3fb-4351-a61c-53398a9631a3", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, + { + "id": "6a0a59a4-4cb6-4ff9-be29-71b9c876d086", + "name": "Delete a retained custom FHIR profile version - default", + "request": { + "urlPathTemplate": "/fhir/profiles/{id}/versions/{version}", + "method": "DELETE", + "pathParameters": { + "id": { + "equalTo": "custom-patient" + }, + "version": { + "equalTo": "2.0.0" + } + } + }, + "response": { + "status": 200, + "body": "\"\"", + "headers": { + "Content-Type": "application/json" + } + }, + "uuid": "6a0a59a4-4cb6-4ff9-be29-71b9c876d086", + "persistent": true, + "priority": 3, + "metadata": { + "mocklab": { + "created": { + "at": "2020-01-01T00:00:00.000Z", + "via": "SYSTEM" + } + } + } + }, { "id": "6630f5de-1d59-422e-8056-7adb27f0b508", "name": "List all summary templates - default", @@ -2479,6 +2844,6 @@ } ], "meta": { - "total": 82 + "total": 93 } } \ No newline at end of file From 0ea42d4b5c940a2d56d221ac73b2bd69fcaf9986 Mon Sep 17 00:00:00 2001 From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:36:23 +0000 Subject: [PATCH 2/4] [fern-autoversion] feat!: promote required fields on ProfileSummary and add profile versioning MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Several previously-optional fields on `ProfileSummary` and related types are now required (non-Optional), a new `versions` sub-client and `ConflictError` are introduced, and the top-level `lang2fhir_batch` module is exported. The field-type promotions are breaking for callers that construct these models directly or rely on `Optional` typing. Key changes: - `ProfileSummary` fields (`id`, `source`, `resource_type`, `url`, `version`, `fhir_version`, `implementation_guide`, `created_at`, `updated_at`) promoted from `Optional` to required; new fields `status`, `date`, and `canonical` added - `ProfileGetResponse.structure_definition` promoted from `Optional[FhirResource]` to required `FhirResource`; `ProfileListResponse.profiles` promoted from `Optional[List[ProfileSummary]]` to required `List[ProfileSummary]` - New `versions` sub-client (`ProfileVersionCreateRequest`, `ProfileVersionListResponse`) added under `phenoml.profiles` for managing retained profile versions - New `ConflictError` (HTTP 409) raised by `profiles.profiles.update()` when a version conflict is detected - New `lang2fhir_batch` module exported from the top-level `phenoml` package 🌿 Generated with Fern --- .fern/metadata.json | 2 +- .fern/replay.lock | 2 +- changelog.md | 13 +++++++++++++ pyproject.toml | 2 +- src/phenoml/core/client_wrapper.py | 2 +- 5 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.fern/metadata.json b/.fern/metadata.json index 1241008..36e9833 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.0.0" } \ No newline at end of file diff --git a/.fern/replay.lock b/.fern/replay.lock index 3238037..92be38d 100644 --- a/.fern/replay.lock +++ b/.fern/replay.lock @@ -133,7 +133,7 @@ patches: [tool.poetry] name = "phenoml" - -version = "0.0.0.dev0" + -version = "17.0.0" +version = "16.11.0" description = "" readme = "README.md" diff --git a/changelog.md b/changelog.md index b68487d..26c9165 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,16 @@ +## [17.0.0] - 2026-09-04 +### Breaking Changes +- **`ProfileSummary`** — fields `id`, `source`, `resource_type`, `url`, `version`, `fhir_version`, `implementation_guide`, `created_at`, and `updated_at` are now required (non-`Optional`); update all construction sites to supply every field and remove any `None`-guard branches. +- **`ProfileGetResponse.structure_definition`** — promoted from `Optional[FhirResource]` to a required `FhirResource`; remove any `None` guard around this field. +- **`ProfileListResponse.profiles`** — promoted from `Optional[List[ProfileSummary]]` to a required `List[ProfileSummary]`; remove any `None` guard around this field. + +### Added +- **`client.lang2fhir_batch`** — new sync and async sub-client (`Lang2FhirBatchClient` / `RawLang2FhirBatchClient`) for managing asynchronous bulk FHIR extraction jobs via `/lang2fhir/batch`; exposes `create`, `upload_item`, `finalize`, `list`, `get`, `get_results`, and `get_result` methods. +- **`phenoml.lang2fhir_batch` models and errors** — new Pydantic response models (`BatchJob`, `JobListResponse`, `JobDetailResponse`, `ResultsPageResponse`, `UploadItemResponse`, and related status types) and eight typed `ApiError` subclasses (e.g., `ConflictError`, `ContentTooLargeError`, `GatewayTimeoutError`) exported from `phenoml.lang2fhir_batch`. +- **`client.profiles.versions`** — new sync and async sub-client for managing immutable StructureDefinition versions on custom profiles; exposes `list`, `create`, `get`, and `delete` methods backed by `ProfileVersionListResponse` and `ProfileVersionCreateRequest`. +- **`ProfileSummary.status`, `.date`, and `.canonical`** — new optional fields surfacing StructureDefinition publication status, authored date, and canonical version-pinned reference. +- **`ConflictError` (profiles)** — new HTTP 409 `ApiError` subclass raised by `client.profiles.profiles.update()` and the profile versions endpoints on version conflicts; `lang2fhir_batch` module is now exported from the top-level `phenoml` package. + ## [16.11.0] - 2026-08-26 ### Added - **`PatientReference`** — new Pydantic model with `system` and `value` fields representing a business identifier for an existing patient; exported from `phenoml.lang2fhir`. diff --git a/pyproject.toml b/pyproject.toml index cea8b20..1de13f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "phenoml" -version = "0.0.0.dev0" +version = "17.0.0" description = "" readme = "README.md" authors = [] diff --git a/src/phenoml/core/client_wrapper.py b/src/phenoml/core/client_wrapper.py index c03a243..2679d1b 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.0.0", **(self.get_custom_headers() or {}), } token = self._get_token() From 424a9e967c19f9b450754d06ba7b99f50231f798 Mon Sep 17 00:00:00 2001 From: "fern-api[bot]" <115122769+fern-api[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:36:24 +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 | 20 +++++++++++++------- pyproject.toml | 3 +++ 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/.fern/replay.lock b/.fern/replay.lock index 92be38d..3f5e899 100644 --- a/.fern/replay.lock +++ b/.fern/replay.lock @@ -114,27 +114,33 @@ generations: cli_version: unknown generator_versions: fernapi/fern-python-sdk: 5.18.1 -current_generation: d943cc568f9ebc7cdbd970a49c9584c685af4816 + - commit_sha: e83d7ca09a7944b6c0248754f21a6ba60fa4629d + tree_hash: 8309b4a27dd63367987407eb1c8b980aa723393a + timestamp: 2026-09-04T16:35:29.404Z + cli_version: unknown + generator_versions: + fernapi/fern-python-sdk: 5.18.1 +current_generation: e83d7ca09a7944b6c0248754f21a6ba60fa4629d patches: - id: patch-6516695e - content_hash: sha256:702e13b522b447abbe0ea94480170f9a906fdab2dbfacd3466b1e92277d4d881 + content_hash: sha256:31f251d4e496ddda9f8277bd39de658824674539e8bba0081e495626fa0165e1 original_commit: 6516695ecaba47ae4bcc8119acca86a1113adeeb original_message: "Release 15.0.2: restore bundled openapi.json packaging (#169)" original_author: Gavin Sharp - base_generation: d943cc568f9ebc7cdbd970a49c9584c685af4816 + base_generation: e83d7ca09a7944b6c0248754f21a6ba60fa4629d files: - pyproject.toml patch_content: | diff --git a/pyproject.toml b/pyproject.toml - index cea8b20..f599fde 100644 + index cea8b20..17a6e91 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ dynamic = ["version"] [tool.poetry] name = "phenoml" - -version = "17.0.0" - +version = "16.11.0" + -version = "0.0.0.dev0" + +version = "17.0.0" description = "" readme = "README.md" authors = [] @@ -156,7 +162,7 @@ patches: [tool.poetry] name = "phenoml" - version = "16.11.0" + version = "17.0.0" description = "" readme = "README.md" authors = [] diff --git a/pyproject.toml b/pyproject.toml index 1de13f3..17a6e91 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 97209989dbcfe4d7ff99550794df9566a1d9d53d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 4 Sep 2026 16:36:38 +0000 Subject: [PATCH 4/4] chore: sync OpenAPI spec + code-examples for e2cfa711e7e5ad04621efc35d12dcfd7d060040d [skip ci] --- code-examples.json | 291 +++++- src/phenoml/openapi/openapi.json | 1447 +++++++++++++++++++++++++++--- 2 files changed, 1625 insertions(+), 113 deletions(-) diff --git a/code-examples.json b/code-examples.json index 587ed6e..b21e89b 100644 --- a/code-examples.json +++ b/code-examples.json @@ -2,8 +2,8 @@ "metadata": { "language": "python", "packageName": "phenoml", - "sdkVersion": "16.11.0", - "specCommit": "4a08550f5db230949c7423d0ce5aa7055e8f0d65", + "sdkVersion": "17.0.0", + "specCommit": "e2cfa711e7e5ad04621efc35d12dcfd7d060040d", "generatorName": "fernapi/fern-python-sdk" }, "renderRules": { @@ -2685,6 +2685,197 @@ } } }, + "POST /lang2fhir/batch": { + "httpMethod": "POST", + "httpPath": "/lang2fhir/batch", + "request": { + "body": { + "request_id": "submit-2025-09-02-batch-001" + } + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2fhir_batch.create({{__body__}})", + "params": [], + "body": { + "fieldSeparator": ", ", + "fields": [ + { + "jsonKey": "request_id", + "fieldTemplate": "request_id={{value}}", + "kind": "string", + "required": false + } + ] + } + } + }, + "GET /lang2fhir/batch": { + "httpMethod": "GET", + "httpPath": "/lang2fhir/batch", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2fhir_batch.list({{__body__}})", + "params": [], + "body": { + "fieldSeparator": ", ", + "fields": [ + { + "jsonKey": "cursor", + "fieldTemplate": "cursor={{value}}", + "kind": "string", + "required": false + }, + { + "jsonKey": "limit", + "fieldTemplate": "limit={{value}}", + "kind": "number", + "required": false + } + ] + } + } + }, + "POST /lang2fhir/batch/{job_id}/items": { + "httpMethod": "POST", + "httpPath": "/lang2fhir/batch/{job_id}/items", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2fhir_batch.upload_item(job_id={{job_id}})", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ] + } + }, + "POST /lang2fhir/batch/{job_id}/finalize": { + "httpMethod": "POST", + "httpPath": "/lang2fhir/batch/{job_id}/finalize", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2fhir_batch.finalize(job_id={{job_id}})", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ] + } + }, + "GET /lang2fhir/batch/{job_id}": { + "httpMethod": "GET", + "httpPath": "/lang2fhir/batch/{job_id}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2fhir_batch.get(job_id={{job_id}}, {{__body__}})", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ], + "body": { + "fieldSeparator": ", ", + "fields": [ + { + "jsonKey": "cursor", + "fieldTemplate": "cursor={{value}}", + "kind": "string", + "required": false + }, + { + "jsonKey": "limit", + "fieldTemplate": "limit={{value}}", + "kind": "number", + "required": false + } + ] + } + } + }, + "GET /lang2fhir/batch/{job_id}/results": { + "httpMethod": "GET", + "httpPath": "/lang2fhir/batch/{job_id}/results", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2fhir_batch.get_results(job_id={{job_id}}, {{__body__}})", + "params": [ + { + "name": "job_id", + "kind": "string" + } + ], + "body": { + "fieldSeparator": ", ", + "fields": [ + { + "jsonKey": "cursor", + "fieldTemplate": "cursor={{value}}", + "kind": "string", + "required": false + }, + { + "jsonKey": "limit", + "fieldTemplate": "limit={{value}}", + "kind": "number", + "required": false + } + ] + } + } + }, + "GET /lang2fhir/batch/{job_id}/results/{item_id}": { + "httpMethod": "GET", + "httpPath": "/lang2fhir/batch/{job_id}/results/{item_id}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.lang2fhir_batch.get_result(job_id={{job_id}}, item_id={{item_id}})", + "params": [ + { + "name": "job_id", + "kind": "string" + }, + { + "name": "item_id", + "kind": "string" + } + ] + } + }, "POST /cohort": { "httpMethod": "POST", "httpPath": "/cohort", @@ -3877,6 +4068,102 @@ ] } }, + "GET /fhir/profiles/{id}/versions": { + "httpMethod": "GET", + "httpPath": "/fhir/profiles/{id}/versions", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.profiles.versions.list(id={{id}})", + "params": [ + { + "name": "id", + "kind": "string" + } + ] + } + }, + "POST /fhir/profiles/{id}/versions": { + "httpMethod": "POST", + "httpPath": "/fhir/profiles/{id}/versions", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.profiles.versions.create(id={{id}}, request={{__body__}})", + "params": [ + { + "name": "id", + "kind": "string" + } + ], + "body": { + "fieldSeparator": ", ", + "fields": [ + { + "jsonKey": "", + "fieldTemplate": "{{value}}", + "kind": "object", + "required": true, + "passthroughBody": true + } + ] + } + } + }, + "GET /fhir/profiles/{id}/versions/{version}": { + "httpMethod": "GET", + "httpPath": "/fhir/profiles/{id}/versions/{version}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.profiles.versions.get(id={{id}}, version={{version}})", + "params": [ + { + "name": "id", + "kind": "string" + }, + { + "name": "version", + "kind": "string" + } + ] + } + }, + "DELETE /fhir/profiles/{id}/versions/{version}": { + "httpMethod": "DELETE", + "httpPath": "/fhir/profiles/{id}/versions/{version}", + "request": { + "body": null + }, + "response": { + "body": null + }, + "render": { + "callTemplate": "client.profiles.versions.delete(id={{id}}, version={{version}})", + "params": [ + { + "name": "id", + "kind": "string" + }, + { + "name": "version", + "kind": "string" + } + ] + } + }, "POST /tools/lang2fhir-and-create": { "httpMethod": "POST", "httpPath": "/tools/lang2fhir-and-create", diff --git a/src/phenoml/openapi/openapi.json b/src/phenoml/openapi/openapi.json index 5e7bdf7..a1d4afd 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": "4a08550f5db230949c7423d0ce5aa7055e8f0d65" + "version": "e2cfa711e7e5ad04621efc35d12dcfd7d060040d" }, "x-services": [ { @@ -4582,6 +4582,491 @@ "x-service": "fhir" } }, + "/lang2fhir/batch": { + "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.\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", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_CreateBatchRequest" + }, + "example": { + "request_id": "submit-2025-09-02-batch-001" + } + } + } + }, + "responses": { + "202": { + "description": "Batch job created (or an idempotent replay of an existing job)", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_BatchJob" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. request_id over its 256-byte limit)" + }, + "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" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + }, + "get": { + "operationId": "batch_list", + "summary": "List batch jobs", + "description": "Returns a page of the instance's batch jobs, newest first, without\nper-job counts. Jobs are shared across the instance's credentials, so\nthis lists every batch job on the instance, not just the calling\ncredential's.\n", + "parameters": [ + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque pagination cursor from a previous page's next_cursor.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size. Defaults to 20; values above 100 are clamped to 100.", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "A page of batch jobs", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_JobListResponse" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. non-integer limit or malformed cursor)" + }, + "401": { + "description": "Unauthorized" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}/items": { + "post": { + "operationId": "batch_uploadItem", + "summary": "Upload one batch item", + "description": "Stores one item of a job from a multipart upload. A batch's items arrive\none per request. The item carries **either** a `document` extraction\n(whose input file rides as raw bytes in the `file` part) **or** a\n`create` extraction (JSON only, no file).\n\nThe upload enforces these rules:\n- Set **exactly one** of `document` or `create`. Setting both, or\n neither, is a `400`.\n- When `document` is set, `file` is **required** \u2014 it supplies the\n document's binary content (PDF or image).\n- When `create` is set, `file` is **forbidden** \u2014 a create item carries\n no file.\n- `document` and `create` must each be a JSON **object**.\n\nOnly the item's structure is checked here: the fields inside `document`\nor `create` are not validated at upload. A body that is well-formed JSON\nbut not a valid request for its endpoint is still accepted with `202`\nand fails later during processing, recorded as an item `error`. A\nwrong-typed field the endpoint cannot decode fails as `invalid_input`; a\nbody that decodes but the pipeline rejects (for example, a missing\nrequired field) fails as `processing_failed`.\n\nSupplying `request_id` makes the upload idempotent on that token. A\nre-upload under the same token overwrites the same item rather than\nadding a second, so a client that lost an upload's response can safely\nre-send it. The response's `deduplicated` is `true` only when the\nre-uploaded payload matches the one already stored; a same-token upload\nwith a changed payload overwrites in place and returns `false`.\n\nSet a `request_id` on **every** upload: re-sending under the same token\nis the only way to repair a lost or incomplete upload, including the one\na finalize `409` reports. Without one, a re-send adds a new item instead\nof replacing the missing one, and the job cannot be finalized.\n\nUploads are rejected once the job has been finalized (`409`), once it\nholds its 500-item limit (`409`), or when the item is too large (`413` \u2014\nsee the raw-file limit in the API description).\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "document": { + "type": "object", + "description": "The JSON body of `POST /lang2fhir/document/multi`, **without**\nits base64 `content` field \u2014 the uploaded `file` supplies the\ncontent. Accepts that endpoint's fields (`version`, `provider`,\n`patient_reference`, `implementation_guide`, `detection_effort`,\n`validation_method`, `config`). This is the **multi**-resource\nbody: it has no single-`resource` field, and the item's result\nis a `DocumentMultiResponse` (a Bundle of resources). Mutually\nexclusive with `create`; requires `file`.\n" + }, + "create": { + "type": "object", + "description": "The JSON body of `POST /lang2fhir/create/multi`. Accepts that\nendpoint's fields (`text`, `version`, `provider`,\n`patient_reference`, `implementation_guide`, `detection_effort`,\n`validation_method`, `resource_review`). This is the\n**multi**-resource body: it has no single-`resource` field, and\nthe item's result is a `CreateMultiResponse` (a Bundle of\nresources). Mutually exclusive with `document`; must **not** be\naccompanied by a `file`.\n" + }, + "file": { + "type": "string", + "format": "binary", + "description": "The document's binary content (PDF, PNG, JPEG, or TIFF).\nRequired with `document`; forbidden with `create`.\n" + }, + "request_id": { + "type": "string", + "maxLength": 256, + "description": "Optional idempotency token (max 256 bytes). Re-uploading under\nthe same token overwrites the same item instead of adding a\nnew one. The token is scoped to this job; the same token in\nanother job is independent and creates a separate item.\n" + }, + "id": { + "type": "string", + "maxLength": 512, + "description": "Optional caller-supplied correlation label (max 512 bytes),\nechoed back on status and result listings so you can match the\nserver's item_id to your own record.\n" + } + } + }, + "encoding": { + "document": { + "contentType": "application/json" + }, + "create": { + "contentType": "application/json" + } + } + } + } + }, + "responses": { + "202": { + "description": "Item accepted", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_UploadItemResponse" + } + } + } + }, + "400": { + "description": "Invalid upload \u2014 e.g. both or neither of document/create set, a\nnon-object document/create, a document item missing its file, a\ncreate item carrying a file, or an over-length request_id/id.\n" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "409": { + "description": "The job is finalized (no longer accepting items) or holds its 500-item limit" + }, + "413": { + "description": "The upload is too large. Both size trips return 413: the request body\nexceeding the 32 MiB upload cap, and the item's stored payload\nexceeding the 28 MiB per-item limit (~21 MiB of raw file once\nbase64-encoded).\n" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}/finalize": { + "post": { + "operationId": "batch_finalize", + "summary": "Finalize a batch job", + "description": "Seals the job's item set and starts processing. Takes no request body.\nFinalize is idempotent: a retried finalize succeeds again.\n\nIf a previous upload did not complete, finalize returns a `409`; re-send\nthe missing upload (with the same `request_id`), then finalize.\nFinalizing a job with no items is a `400`.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "202": { + "description": "Job finalized and processing started", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_BatchJob" + } + } + } + }, + "400": { + "description": "The batch has no items to finalize" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "409": { + "description": "A previous upload did not complete; re-send it with the same request_id, then finalize" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}": { + "get": { + "operationId": "batch_get", + "summary": "Get a batch job", + "description": "Returns a job's record, its per-status item counts, and one page of\nper-item statuses.\n\nItems are listed in a stable order that is not upload order and is the\nsame across pages. Match each entry to your own records by its `id`\n(your correlation label) or `item_id` (from the upload response),\nnever by position.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque pagination cursor from a previous page's next_cursor.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size for the item-status page. Defaults to 20; values above 100 are clamped to 100.", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "The job record, counts, and a page of item statuses", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_JobDetailResponse" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. non-integer limit or malformed cursor)" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}/results": { + "get": { + "operationId": "batch_getResults", + "summary": "List a batch job's results", + "description": "A lighter status page. Returns the same per-item status entries as\n`GET /lang2fhir/batch/{job_id}`, but without the job record or counts,\nand the entries carry `result_size` rather than any result content. Use\neach entry's `item_id` to fetch that item's result from\n`GET /lang2fhir/batch/{job_id}/results/{item_id}`.\n\nEntries are listed in a stable order that is not upload order and is\nthe same across pages. Match each entry to your own records by its `id`\n(your correlation label) or `item_id` (from the upload response),\nnever by position.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "cursor", + "in": "query", + "required": false, + "description": "Opaque pagination cursor from a previous page's next_cursor.", + "schema": { + "type": "string" + } + }, + { + "name": "limit", + "in": "query", + "required": false, + "description": "Page size. Defaults to 20; values above 100 are clamped to 100.", + "schema": { + "type": "integer", + "default": 20, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "A page of per-item result statuses", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/batch_ResultsPageResponse" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. non-integer limit or malformed cursor)" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job not found" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, + "/lang2fhir/batch/{job_id}/results/{item_id}": { + "get": { + "operationId": "batch_getResult", + "summary": "Get one batch item's result", + "description": "Streams one item's stored result bytes verbatim as `application/json`.\nThe body is the response the item's synchronous multi endpoint would have\nreturned \u2014 a `DocumentMultiResponse` for a document item or a\n`CreateMultiResponse` for a create item.\n\nOnly a succeeded item has a result: an item that has not succeeded\n(pending, processing, or failed) is a `409`, and a result that has\nexpired is a `404`.\n", + "parameters": [ + { + "name": "job_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + }, + { + "name": "item_id", + "in": "path", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "The item's raw result JSON", + "content": { + "application/json": { + "schema": { + "type": "object", + "description": "The item's extraction result \u2014 a DocumentMultiResponse (document\nitem) or CreateMultiResponse (create item).\n" + } + } + } + }, + "400": { + "description": "Invalid request (e.g. empty item_id)" + }, + "401": { + "description": "Unauthorized" + }, + "404": { + "description": "Batch job or item not found, or the result has expired" + }, + "409": { + "description": "The item has not succeeded, so it has no result to serve" + }, + "499": { + "description": "Client closed request before response was ready" + }, + "500": { + "description": "Server error" + }, + "504": { + "description": "Request timed out" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "lang2fhir" + } + }, "/cohort": { "post": { "operationId": "cohort_analyze", @@ -4787,7 +5272,7 @@ "description": "Profile not found" }, "422": { - "description": "Generated FHIR resource failed validation" + "description": "Generated FHIR resource failed validation or terminology constraints" }, "500": { "description": "Server error" @@ -4907,7 +5392,7 @@ "description": "Profile not found" }, "422": { - "description": "Generated FHIR resource failed validation" + "description": "Generated FHIR resource failed validation or terminology constraints" }, "500": { "description": "Server error" @@ -6073,13 +6558,13 @@ "FHIR Artifacts / Profiles" ], "summary": "List custom FHIR profiles", - "description": "Returns metadata for every custom (uploaded) FHIR profile on this\ninstance, across all implementation guides. The full StructureDefinition\nJSON is omitted from each entry; fetch a single profile by id to retrieve it.\n\nThe `url` query parameter filters by canonical URL. The canonical URL is the\nstable key other platform features use to reference a profile (FHIR's\n`meta.profile`, `baseDefinition`), since StructureDefinition ids are only\nunique within a package. A non-matching filter returns an empty list, not a 404.\n", + "description": "Returns metadata for every custom (uploaded) FHIR profile on this\ninstance, across all implementation guides. The full StructureDefinition\nJSON is omitted from each entry; fetch a single profile by id to retrieve it.\n\nThe `url` query parameter filters by canonical URL. The canonical URL is the\nstable key other platform features use to reference a profile (FHIR's\n`meta.profile`, `baseDefinition`), since StructureDefinition ids are only\nunique within a package. An unpinned `url` filter returns metadata for\nthe profile's current StructureDefinition. Pinned `url|version` filters\nresolve a retained version when present; otherwise they can fall back to\nthe profile's current StructureDefinition, whose content can change\nthrough the profile update endpoint. A non-matching filter returns an\nempty list, not a 404.\n", "parameters": [ { "name": "url", "in": "query", "required": false, - "description": "Filter by canonical URL. Accepts the FHIR pinned form `url|version` (split on the last `|`); the bare form matches the current version.\n", + "description": "Filter by canonical URL. Accepts the FHIR pinned form `url|version`; without a version pin, returns the profile's current StructureDefinition metadata.\n", "schema": { "type": "string" }, @@ -6120,7 +6605,7 @@ "FHIR Artifacts / Profiles" ], "summary": "Upload a custom FHIR profile", - "description": "Creates a custom profile from a FHIR StructureDefinition supplied as a JSON\nobject. Metadata such as version, resource type, and url is read from the\nStructureDefinition; the lowercase StructureDefinition id becomes the\nprofile's lookup key. When id is omitted, a random UUID is assigned. Code\nsystem configuration is auto-extracted from the snapshot. Optionally group\nthe profile under a named implementation guide.\n", + "description": "Creates a custom profile from a FHIR StructureDefinition supplied as a JSON\nobject. Metadata such as version, resource type, and url is read from the\nStructureDefinition; the lowercase StructureDefinition id becomes the\nprofile's lookup key. When id is omitted, a random UUID is assigned.\nOptionally group the profile under a named implementation guide.\n", "requestBody": { "required": true, "content": { @@ -6143,7 +6628,147 @@ } }, "400": { - "description": "Invalid request or profile validation failed. Possible reasons:\n- Missing or invalid StructureDefinition JSON\n- The id collides with a built-in US Core or R4 base profile\n- A custom profile with the same id or url already exists\n" + "description": "Invalid request or profile validation failed. Possible reasons:\n- Missing or invalid StructureDefinition JSON\n- The id collides with a built-in US Core or R4 base profile\n- A custom profile with the same id or url already exists\n- The implementation_guide is reserved or invalid\n" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - custom profiles are only available on dedicated instances" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + } + }, + "/fhir/profiles/{id}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The lowercase StructureDefinition id of the custom profile.", + "schema": { + "type": "string" + }, + "example": "custom-patient" + } + ], + "get": { + "operationId": "profiles_get", + "tags": [ + "FHIR Artifacts / Profiles" + ], + "summary": "Get a custom FHIR profile", + "description": "Returns a single custom profile by id, including its full StructureDefinition\nJSON.\n", + "responses": { + "200": { + "description": "The requested profile", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/profiles_ProfileGetResponse" + } + } + } + }, + "400": { + "description": "The id is not a valid profile id" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - custom profiles are only available on dedicated instances" + }, + "404": { + "description": "Profile not found" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + }, + "put": { + "operationId": "profiles_update", + "tags": [ + "FHIR Artifacts / Profiles" + ], + "summary": "Update a custom FHIR profile", + "description": "Replaces an existing custom profile with a new StructureDefinition. The\n`id` path parameter is authoritative: if the StructureDefinition includes\nan `id` it must match the path parameter, and if it omits one the path\nparameter is used. The FHIR resource type of the profile cannot change.\nWhen `implementation_guide` is omitted, the profile keeps its existing\nimplementation guide. A retained version string is allowed only when\nre-submitting the profile's current version with an unchanged\nStructureDefinition; otherwise it returns a conflict. While the profile\nhas retained versions, its\ncanonical URL cannot be changed.\n", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/profiles_ProfileUploadRequest" + } + } + } + }, + "responses": { + "200": { + "description": "Profile successfully updated", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/profiles_ProfileSummary" + } + } + } + }, + "400": { + "description": "Invalid request or profile validation failed. Possible reasons:\n- Missing or invalid StructureDefinition JSON\n- The StructureDefinition id does not match the id path parameter\n- The resource type differs from the existing profile\n- The url collides with a built-in profile or another custom profile\n- The implementation_guide is reserved or invalid\n" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - custom profiles are only available on dedicated instances" + }, + "404": { + "description": "Profile not found" + }, + "409": { + "description": "Conflict. Possible reasons:\n- The requested StructureDefinition.version is already used by a retained version and this is not an unchanged re-submission of the profile's current version\n- The profile has retained versions and the request changes its canonical URL\n" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + }, + "delete": { + "operationId": "profiles_delete", + "tags": [ + "FHIR Artifacts / Profiles" + ], + "summary": "Delete a custom FHIR profile", + "description": "Permanently deletes a custom profile by id. This also deletes all retained\nversions for that profile so the canonical URL can be reused by a later\nupload.\n", + "responses": { + "204": { + "description": "Profile successfully deleted" + }, + "400": { + "description": "The id is not a valid profile id" }, "401": { "description": "Unauthorized" @@ -6151,6 +6776,9 @@ "403": { "description": "Forbidden - custom profiles are only available on dedicated instances" }, + "404": { + "description": "Profile not found" + }, "500": { "description": "Server error" } @@ -6163,7 +6791,7 @@ "x-service": "fhir_artifacts" } }, - "/fhir/profiles/{id}": { + "/fhir/profiles/{id}/versions": { "parameters": [ { "name": "id", @@ -6177,19 +6805,19 @@ } ], "get": { - "operationId": "profiles_get", + "operationId": "profiles_listVersions", "tags": [ "FHIR Artifacts / Profiles" ], - "summary": "Get a custom FHIR profile", - "description": "Returns a single custom profile by id, including its full StructureDefinition JSON.\n", + "summary": "List versions for a custom FHIR profile", + "description": "Returns retained versions for a custom profile.\n", "responses": { "200": { - "description": "The requested profile", + "description": "List of profile versions", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/profiles_ProfileGetResponse" + "$ref": "#/components/schemas/profiles_ProfileVersionListResponse" } } } @@ -6217,26 +6845,35 @@ ], "x-service": "fhir_artifacts" }, - "put": { - "operationId": "profiles_update", + "post": { + "operationId": "profiles_createVersion", "tags": [ "FHIR Artifacts / Profiles" ], - "summary": "Update a custom FHIR profile", - "description": "Replaces an existing custom profile with a new StructureDefinition. The\n`id` path parameter is authoritative: if the StructureDefinition includes\nan `id` it must match the path parameter, and if it omits one the path\nparameter is used. The FHIR resource type of the profile cannot change.\nCode system configuration is\nre-derived from the new StructureDefinition. When `implementation_guide` is\nomitted, the profile keeps its existing implementation guide. The instance\nstores a single version per canonical URL, so this replaces it in place.\n", + "summary": "Add a retained version to a custom FHIR profile", + "description": "Adds an immutable StructureDefinition version to a custom profile. If\nthe profile does not exist, it is created from the submitted version.\nThe StructureDefinition must include a non-empty `version`; its\ncanonical URL and resource type must match the profile when one already\nexists. If it includes an `id`, that id must match the path parameter;\nif it omits `id`, the path parameter is used. Profiles created through\nthis endpoint are grouped under `custom`. Posting the profile's current\nStructureDefinition unchanged retains it as a version.\nVersion strings may contain letters, numbers, and the punctuation\ncharacters `.`, `_`, `~`, `+`, and `-`; they cannot be exactly `.` or\n`..`. Each profile can retain up to 250 versions; delete old\nversions before adding more.\n", "requestBody": { "required": true, "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/profiles_ProfileUploadRequest" + "$ref": "#/components/schemas/profiles_ProfileVersionCreateRequest" } } } }, "responses": { - "200": { - "description": "Profile successfully updated", + "201": { + "description": "Profile version successfully created", + "headers": { + "Location": { + "description": "Relative URL of the created profile version.", + "schema": { + "type": "string" + }, + "example": "/fhir/profiles/custom-patient/versions/2.0.0" + } + }, "content": { "application/json": { "schema": { @@ -6246,7 +6883,7 @@ } }, "400": { - "description": "Invalid request or profile validation failed. Possible reasons:\n- Missing or invalid StructureDefinition JSON\n- The StructureDefinition id does not match the id path parameter\n- The resource type differs from the existing profile\n- The url collides with a built-in profile or another custom profile\n" + "description": "Invalid request, version does not match the profile, or the canonical URL is already used by another profile.\n" }, "401": { "description": "Unauthorized" @@ -6257,6 +6894,75 @@ "404": { "description": "Profile not found" }, + "409": { + "description": "Conflict. Possible reasons:\n- The requested StructureDefinition.version is already in use by a different StructureDefinition: either the profile's current one or a retained version\n- The profile has reached the retained version limit\n" + }, + "500": { + "description": "Server error" + } + }, + "security": [ + { + "bearerAuth": [] + } + ], + "x-service": "fhir_artifacts" + } + }, + "/fhir/profiles/{id}/versions/{version}": { + "parameters": [ + { + "name": "id", + "in": "path", + "required": true, + "description": "The lowercase StructureDefinition id of the custom profile.", + "schema": { + "type": "string" + }, + "example": "custom-patient" + }, + { + "name": "version", + "in": "path", + "required": true, + "description": "The authored StructureDefinition.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": "2.0.0" + } + ], + "get": { + "operationId": "profiles_getVersion", + "tags": [ + "FHIR Artifacts / Profiles" + ], + "summary": "Get a retained custom FHIR profile version", + "description": "Returns metadata and the full StructureDefinition for one retained\nversion. The returned StructureDefinition's id is the profile id. The\npath version is the authored `StructureDefinition.version` value.\n", + "responses": { + "200": { + "description": "The requested profile version", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/profiles_ProfileGetResponse" + } + } + } + }, + "400": { + "description": "The id or version is invalid" + }, + "401": { + "description": "Unauthorized" + }, + "403": { + "description": "Forbidden - custom profiles are only available on dedicated instances" + }, + "404": { + "description": "Profile or version not found" + }, "500": { "description": "Server error" } @@ -6269,18 +6975,18 @@ "x-service": "fhir_artifacts" }, "delete": { - "operationId": "profiles_delete", + "operationId": "profiles_deleteVersion", "tags": [ "FHIR Artifacts / Profiles" ], - "summary": "Delete a custom FHIR profile", - "description": "Permanently deletes a custom profile by id.\n", + "summary": "Delete a retained custom FHIR profile version", + "description": "Deletes one retained version from a custom profile. The path\nversion is the authored `StructureDefinition.version` value.\n", "responses": { "204": { - "description": "Profile successfully deleted" + "description": "Profile version successfully deleted" }, "400": { - "description": "The id is not a valid profile id" + "description": "The id or version is invalid" }, "401": { "description": "Unauthorized" @@ -6289,7 +6995,7 @@ "description": "Forbidden - custom profiles are only available on dedicated instances" }, "404": { - "description": "Profile not found" + "description": "Profile or version not found" }, "500": { "description": "Server error" @@ -10911,115 +11617,426 @@ } } }, - "fhir_provider_NoAuth": { + "fhir_provider_NoAuth": { + "type": "object", + "required": [ + "auth_method" + ], + "properties": { + "auth_method": { + "type": "string", + "enum": [ + "none" + ] + } + } + }, + "fhir_provider_FhirProviderSetActiveAuthConfigRequest": { + "type": "object", + "required": [ + "auth_config_id" + ], + "properties": { + "auth_config_id": { + "type": "string", + "description": "ID of the auth configuration to set as active", + "example": "auth-config-123" + } + } + }, + "fhir_provider_FhirProviderRemoveAuthConfigRequest": { + "type": "object", + "required": [ + "auth_config_id" + ], + "properties": { + "auth_config_id": { + "type": "string", + "description": "ID of the auth configuration to remove", + "example": "auth-config-123" + } + } + }, + "fhir_provider_FhirQueryResponse": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "status": { + "type": "integer", + "description": "HTTP status code from the FHIR server response", + "example": 200 + }, + "message": { + "type": "string", + "example": "Fhir provider searched successfully" + }, + "data": { + "oneOf": [ + { + "type": "object", + "description": "FHIR resource (Patient, Observation, Condition, etc.)", + "additionalProperties": true, + "example": { + "resourceType": "Patient", + "id": "example-patient", + "name": [ + { + "family": "Doe", + "given": [ + "John" + ] + } + ], + "gender": "male", + "birthDate": "1970-01-01" + } + }, + { + "type": "object", + "description": "FHIR Bundle containing multiple resources", + "additionalProperties": true, + "example": { + "resourceType": "Bundle", + "type": "searchset", + "total": 1, + "entry": [ + { + "resource": { + "resourceType": "Patient", + "id": "example-patient", + "name": [ + { + "family": "Doe", + "given": [ + "John" + ] + } + ] + } + } + ] + } + }, + { + "type": "string", + "description": "Raw response text if not valid JSON", + "example": "Error: Invalid response format" + } + ] + } + } + }, + "batch_CreateBatchRequest": { + "type": "object", + "description": "Body of a create-batch request. Opens an empty job.", + "properties": { + "request_id": { + "type": "string", + "description": "Optional client idempotency token. A retried create with the same\ntoken returns the original job instead of opening a second one.\n", + "example": "submit-2025-09-02-batch-001" + } + } + }, + "batch_BatchError": { + "type": "object", + "description": "A job- or item-level failure. Never contains document or patient content.", + "required": [ + "kind", + "message" + ], + "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", + "example": "processing_failed" + }, + "message": { + "type": "string", + "description": "Human-readable description of the failure.", + "example": "the item could not be converted" + } + } + }, + "batch_BatchJob": { + "type": "object", + "description": "One batch job record.", + "required": [ + "job_id", + "status", + "finalized", + "total_items", + "created_at", + "updated_at", + "expires_at" + ], + "properties": { + "job_id": { + "type": "string", + "description": "Server-assigned job identifier.", + "example": "l2f_batch_6f1d2c3a-8b4e-4f5a-9c7d-0e1f2a3b4c5d" + }, + "request_id": { + "type": "string", + "description": "The idempotency token supplied at create, if any.", + "example": "submit-2025-09-02-batch-001" + }, + "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.\n", + "enum": [ + "pending", + "processing", + "completed", + "failed" + ], + "example": "pending" + }, + "finalized": { + "type": "boolean", + "description": "Whether the job's item set has been sealed.", + "example": false + }, + "total_items": { + "type": "integer", + "description": "The sealed item count. It is 0 until the job is finalized, so an\nupload response always reports 0; poll the job after finalize for the\nreal count.\n", + "example": 12 + }, + "error": { + "description": "A whole-job failure. Present only on a `failed` job.", + "allOf": [ + { + "$ref": "#/components/schemas/batch_BatchError" + } + ] + }, + "created_at": { + "type": "string", + "format": "date-time" + }, + "updated_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "When the job finished. Absent until then." + }, + "expires_at": { + "type": "string", + "format": "date-time", + "description": "When the job and its stored inputs and results are deleted. Set 7\ndays out, with the clock restarting when the job reaches `completed`\nor `failed`. At expiry the job's `request_id` is freed for reuse.\n" + } + } + }, + "batch_BatchCounts": { "type": "object", + "description": "A job's per-status item tally.", "required": [ - "auth_method" + "total", + "pending", + "processing", + "succeeded", + "failed" ], "properties": { - "auth_method": { - "type": "string", - "enum": [ - "none" - ] + "total": { + "type": "integer", + "example": 12 + }, + "pending": { + "type": "integer", + "example": 3 + }, + "processing": { + "type": "integer", + "example": 2 + }, + "succeeded": { + "type": "integer", + "example": 6 + }, + "failed": { + "type": "integer", + "example": 1 } } }, - "fhir_provider_FhirProviderSetActiveAuthConfigRequest": { + "batch_BatchItemStatus": { "type": "object", + "description": "One item's status line.", "required": [ - "auth_config_id" + "item_id", + "status", + "attempts" ], "properties": { - "auth_config_id": { + "item_id": { "type": "string", - "description": "ID of the auth configuration to set as active", - "example": "auth-config-123" + "description": "Server-assigned opaque item ID, used in result URLs.", + "example": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + }, + "id": { + "type": "string", + "description": "The caller's correlation label, echoed back; omitted when none was supplied.", + "example": "chart-note-0042" + }, + "status": { + "type": "string", + "description": "Item status.", + "enum": [ + "pending", + "processing", + "succeeded", + "failed" + ], + "example": "succeeded" + }, + "attempts": { + "type": "integer", + "description": "Number of processing attempts so far. An item runs up to 3 attempts,\nand only an interruption \u2014 a worker preemption or a recovered internal\nerror \u2014 is retried; a conversion error fails the item with no retry.\n", + "example": 1 + }, + "detect_retries": { + "type": "integer", + "format": "int64", + "description": "How many times concept detection was re-run for the item because a\npass returned no concepts. Present only on a succeeded item whose\ndetection re-ran at least once; omitted otherwise.\n", + "example": 1 + }, + "result_size": { + "type": "integer", + "format": "int64", + "description": "Size in bytes of the item's stored result. Present once the item has succeeded.", + "example": 20482 + }, + "error": { + "$ref": "#/components/schemas/batch_BatchError" + }, + "completed_at": { + "type": "string", + "format": "date-time", + "description": "When the item finished. Absent until then." } } }, - "fhir_provider_FhirProviderRemoveAuthConfigRequest": { + "batch_UploadItemResponse": { + "description": "The upload response: the job record plus the ID the server assigned the\nuploaded item.\n", + "allOf": [ + { + "$ref": "#/components/schemas/batch_BatchJob" + }, + { + "type": "object", + "required": [ + "item_id", + "deduplicated" + ], + "properties": { + "item_id": { + "type": "string", + "description": "The server-assigned ID of the uploaded item; use it to fetch the result.", + "example": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08" + }, + "id": { + "type": "string", + "description": "The caller's correlation label, echoed back; omitted when none was supplied.", + "example": "chart-note-0042" + }, + "deduplicated": { + "type": "boolean", + "description": "True only when a repeated `request_id` resolved to an item whose\npayload matched the one already stored. A same-token upload with a\nchanged payload overwrites in place and returns false.\n", + "example": false + } + } + } + ] + }, + "batch_JobDetailResponse": { + "description": "A job's record, its counts, and one page of item statuses.\n", + "allOf": [ + { + "$ref": "#/components/schemas/batch_BatchJob" + }, + { + "type": "object", + "required": [ + "counts", + "items", + "has_more" + ], + "properties": { + "counts": { + "$ref": "#/components/schemas/batch_BatchCounts" + }, + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/batch_BatchItemStatus" + } + }, + "next_cursor": { + "type": "string", + "description": "Cursor for the next page, when has_more is true." + }, + "has_more": { + "type": "boolean", + "description": "Whether more item statuses remain beyond this page.", + "example": false + } + } + } + ] + }, + "batch_ResultsPageResponse": { "type": "object", + "description": "A page of per-item result statuses, without the job counts.", "required": [ - "auth_config_id" + "results", + "has_more" ], "properties": { - "auth_config_id": { + "results": { + "type": "array", + "items": { + "$ref": "#/components/schemas/batch_BatchItemStatus" + } + }, + "next_cursor": { "type": "string", - "description": "ID of the auth configuration to remove", - "example": "auth-config-123" + "description": "Cursor for the next page, when has_more is true." + }, + "has_more": { + "type": "boolean", + "description": "Whether more results remain beyond this page.", + "example": false } } }, - "fhir_provider_FhirQueryResponse": { + "batch_JobListResponse": { "type": "object", + "description": "A page of job records, without per-job counts.", + "required": [ + "jobs", + "has_more" + ], "properties": { - "success": { - "type": "boolean", - "example": true - }, - "status": { - "type": "integer", - "description": "HTTP status code from the FHIR server response", - "example": 200 + "jobs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/batch_BatchJob" + } }, - "message": { + "next_cursor": { "type": "string", - "example": "Fhir provider searched successfully" + "description": "Cursor for the next page, when has_more is true." }, - "data": { - "oneOf": [ - { - "type": "object", - "description": "FHIR resource (Patient, Observation, Condition, etc.)", - "additionalProperties": true, - "example": { - "resourceType": "Patient", - "id": "example-patient", - "name": [ - { - "family": "Doe", - "given": [ - "John" - ] - } - ], - "gender": "male", - "birthDate": "1970-01-01" - } - }, - { - "type": "object", - "description": "FHIR Bundle containing multiple resources", - "additionalProperties": true, - "example": { - "resourceType": "Bundle", - "type": "searchset", - "total": 1, - "entry": [ - { - "resource": { - "resourceType": "Patient", - "id": "example-patient", - "name": [ - { - "family": "Doe", - "given": [ - "John" - ] - } - ] - } - } - ] - } - }, - { - "type": "string", - "description": "Raw response text if not valid JSON", - "example": "Error: Invalid response format" - } - ] + "has_more": { + "type": "boolean", + "description": "Whether more jobs remain beyond this page.", + "example": false } } }, @@ -12064,6 +13081,32 @@ "required": [ "structure_definition" ], + "example": { + "implementation_guide": "acme-cardiology", + "structure_definition": { + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + { + "id": "Patient", + "path": "Patient", + "min": 0, + "max": "*" + } + ] + } + } + }, "properties": { "structure_definition": { "allOf": [ @@ -12111,6 +13154,33 @@ }, "profiles_ProfileSummary": { "type": "object", + "description": "Metadata for either a custom profile's current StructureDefinition or one retained version. For retained versions, `source` is always `custom` and `updated_at` is equal to `created_at`.\n", + "required": [ + "id", + "source", + "resource_type", + "url", + "version", + "canonical", + "fhir_version", + "implementation_guide", + "created_at", + "updated_at" + ], + "example": { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "status": "active", + "date": "2026-08-24", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-24T15:04:05Z", + "updated_at": "2026-08-25T16:04:05Z" + }, "properties": { "id": { "type": "string", @@ -12123,7 +13193,7 @@ "custom", "builtin" ], - "description": "The profile's origin. Listings currently return only custom (uploaded) profiles, so this is always \"custom\" today; built-in (US Core / R4 base) profiles would be surfaced via an opt-in parameter in a future release, not by changing the default behavior.\n", + "description": "The profile's origin. Profile management responses currently return custom (uploaded) profiles, so this is always \"custom\" today.\n", "example": "custom" }, "resource_type": { @@ -12141,6 +13211,21 @@ "description": "The version from the StructureDefinition.version field.", "example": "1.0.0" }, + "status": { + "type": "string", + "description": "The publication status from StructureDefinition.status. Expected FHIR values include `draft`, `active`, `retired`, and `unknown`; the server preserves authored strings.\n", + "example": "active" + }, + "date": { + "type": "string", + "description": "The authored publication date from StructureDefinition.date, when present. This is a FHIR dateTime string and may be less precise than a full timestamp.\n", + "example": "2026-08-24" + }, + "canonical": { + "type": "string", + "description": "The canonical profile reference, including the version pin when present.", + "example": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0" + }, "fhir_version": { "type": "string", "description": "The base FHIR version the StructureDefinition targets.", @@ -12157,12 +13242,34 @@ }, "updated_at": { "type": "string", - "format": "date-time" + "format": "date-time", + "description": "Last update timestamp for the profile's current StructureDefinition. For retained versions, this equals `created_at`.\n" } } }, "profiles_ProfileListResponse": { "type": "object", + "required": [ + "profiles" + ], + "example": { + "profiles": [ + { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "status": "active", + "date": "2026-08-24", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-24T15:04:05Z", + "updated_at": "2026-08-25T16:04:05Z" + } + ] + }, "properties": { "profiles": { "type": "array", @@ -12173,12 +13280,54 @@ } }, "profiles_ProfileGetResponse": { + "description": "Metadata and full StructureDefinition JSON for either a custom profile's current StructureDefinition or one retained version.\n", + "example": { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "status": "active", + "date": "2026-08-24", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-24T15:04:05Z", + "updated_at": "2026-08-25T16:04:05Z", + "structure_definition": { + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + { + "id": "Patient", + "path": "Patient", + "min": 0, + "max": "*" + } + ] + } + } + }, "allOf": [ { "$ref": "#/components/schemas/profiles_ProfileSummary" }, { "type": "object", + "required": [ + "structure_definition" + ], "properties": { "structure_definition": { "allOf": [ @@ -12192,6 +13341,82 @@ } ] }, + "profiles_ProfileVersionCreateRequest": { + "allOf": [ + { + "$ref": "#/components/schemas/profiles_FHIRResource" + } + ], + "description": "A FHIR StructureDefinition as a JSON object. Must include resourceType: StructureDefinition, url, type, version, and a snapshot with elements. The canonical URL and resource type must match the profile when one already exists. If id is present, its lowercased value must match the path id; if omitted, the path id is used. The version may contain letters, numbers, and the punctuation characters `.`, `_`, `~`, `+`, and `-`; it cannot be exactly `.` or `..`.\n", + "example": { + "resourceType": "StructureDefinition", + "id": "custom-patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "2.0.0", + "name": "CustomPatient", + "status": "active", + "fhirVersion": "4.0.1", + "kind": "resource", + "abstract": false, + "type": "Patient", + "baseDefinition": "http://hl7.org/fhir/StructureDefinition/Patient", + "derivation": "constraint", + "snapshot": { + "element": [ + { + "id": "Patient", + "path": "Patient" + } + ] + } + } + }, + "profiles_ProfileVersionListResponse": { + "type": "object", + "required": [ + "versions" + ], + "example": { + "versions": [ + { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "2.0.0", + "status": "active", + "date": "2026-08-26", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|2.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-26T15:04:05Z", + "updated_at": "2026-08-26T15:04:05Z" + }, + { + "id": "custom-patient", + "source": "custom", + "resource_type": "Patient", + "url": "http://phenoml.com/fhir/StructureDefinition/custom-patient", + "version": "1.0.0", + "status": "active", + "date": "2026-08-24", + "canonical": "http://phenoml.com/fhir/StructureDefinition/custom-patient|1.0.0", + "fhir_version": "4.0.1", + "implementation_guide": "acme-cardiology", + "created_at": "2026-08-24T15:04:05Z", + "updated_at": "2026-08-24T15:04:05Z" + } + ] + }, + "properties": { + "versions": { + "type": "array", + "items": { + "$ref": "#/components/schemas/profiles_ProfileSummary" + } + } + } + }, "profiles_FHIRResource": { "type": "object", "description": "A FHIR resource (schema depends on resource type)" @@ -13482,7 +14707,7 @@ }, { "name": "FHIR Artifacts / Profiles", - "description": "Manage custom FHIR profiles (StructureDefinitions) as instance-level FHIR artifacts." + "description": "Manage custom FHIR profiles (StructureDefinitions)." }, { "name": "Tools / MCP Servers",