From a8ccf28efd8002a4b22ab0034bd01c156fea63c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Wed, 29 Jul 2026 13:15:19 -0300 Subject: [PATCH 1/2] pin ruff and ty version --- .pre-commit-config.yaml | 6 +++--- docs/DEVELOPMENT.md | 7 +++++-- pyproject.toml | 4 ++-- uv.lock | 45 +++++++++++++++++++++-------------------- 4 files changed, 33 insertions(+), 29 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a25f8e86..93dae0af 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -9,17 +9,17 @@ repos: hooks: - id: ruff-format name: ruff format - entry: uvx ruff format --force-exclude --check + entry: uv run ruff format --force-exclude --check language: system types: [python] - id: ruff-check name: ruff check - entry: uvx ruff check --force-exclude --fix + entry: uv run ruff check --force-exclude --fix language: system types: [python] - id: ty-check name: ty type check - entry: uvx ty check . + entry: uv run ty check . language: system types: [python] pass_filenames: false diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 02a22ffa..8c54d5d9 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -34,7 +34,7 @@ Tip: ## Type Check ```bash -uvx ty check . +uv run ty check . ``` ## Code Quality Checks @@ -49,9 +49,12 @@ uv run ruff check . uv run ruff format --check . # Type check -uvx ty check . +uv run ty check . ``` +> **Linter versions are pinned.** `ruff` and `ty` are pinned to exact versions in the +> `pyproject.toml` `[dependency-groups] dev` section (and locked in `uv.lock`) for consistent runs across local and CI. + ## Pre-commit Hooks (Recommended) Automatically run quality checks before each commit using pre-commit hooks. diff --git a/pyproject.toml b/pyproject.toml index c8b960bf..15534602 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -63,9 +63,9 @@ dev = [ "pytest-asyncio>=1.0.0", "pytest-bdd>=7.2.0", "python-dotenv>=1.0.0", - "ty>=0.0.21", + "ty==0.0.64", "cryptography>=46.0.3", - "ruff>=0.8.0", + "ruff==0.15.5", "starlette>=0.40.0", "anyio>=3.6.2", "httpx>=0.27.0", diff --git a/uv.lock b/uv.lock index ebefbc1a..6b2cf249 100644 --- a/uv.lock +++ b/uv.lock @@ -4073,10 +4073,10 @@ dev = [ { name = "pytest-bdd", specifier = ">=7.2.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, - { name = "ruff", specifier = ">=0.8.0" }, + { name = "ruff", specifier = "==0.15.5" }, { name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "starlette", specifier = ">=0.40.0" }, - { name = "ty", specifier = ">=0.0.21" }, + { name = "ty", specifier = "==0.0.64" }, ] [[package]] @@ -4410,26 +4410,27 @@ wheels = [ [[package]] name = "ty" -version = "0.0.21" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ee/20/2ba8fd9493c89c41dfe9dbb73bc70a28b28028463bc0d2897ba8be36230a/ty-0.0.21.tar.gz", hash = "sha256:a4c2ba5d67d64df8fcdefd8b280ac1149d24a73dbda82fa953a0dff9d21400ed", size = 5297967, upload-time = "2026-03-06T01:57:13.809Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/36/70/edf38bb37517531681d1c37f5df64744e5ad02673c02eb48447eae4bea08/ty-0.0.21-py3-none-linux_armv6l.whl", hash = "sha256:7bdf2f572378de78e1f388d24691c89db51b7caf07cf90f2bfcc1d6b18b70a76", size = 10299222, upload-time = "2026-03-06T01:57:16.64Z" }, - { url = "https://files.pythonhosted.org/packages/72/62/0047b0bd19afeefbc7286f20a5f78a2aa39f92b4d89853f0d7185ab89edc/ty-0.0.21-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7e9613994610431ab8625025bd2880dbcb77c5c9fabdd21134cda12d840a529d", size = 10130513, upload-time = "2026-03-06T01:57:29.93Z" }, - { url = "https://files.pythonhosted.org/packages/a2/20/0b93a9e91aaed23155780258cdfdb4726ef68b6985378ac069bc427291a0/ty-0.0.21-py3-none-macosx_11_0_arm64.whl", hash = "sha256:56d3b198b64dd0a19b2b66e257deaed2ecea568e722ae5352f3c6fb62027f89d", size = 9605425, upload-time = "2026-03-06T01:57:27.115Z" }, - { url = "https://files.pythonhosted.org/packages/ea/fd/9945e2fa2996a1287b1e1d7ce050e97e1f420233b271e770934bfa0880a0/ty-0.0.21-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d23d2c34f7a77d974bb08f0860ef700addc8a683d81a0319f71c08f87506cfd0", size = 10108298, upload-time = "2026-03-06T01:57:35.429Z" }, - { url = "https://files.pythonhosted.org/packages/52/e7/4ec52fcb15f3200826c9f048472c062549a05b0d1ef0b51f32d527b513c4/ty-0.0.21-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:56b01fd2519637a4ca88344f61c96225f540c98ff18bca321d4eaa7bb0f7aa2f", size = 10121556, upload-time = "2026-03-06T01:57:03.242Z" }, - { url = "https://files.pythonhosted.org/packages/ee/c0/ad457be2a8abea0f25549598bd098554540ced66229488daa0d558dad3c8/ty-0.0.21-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e9de7e11c63c6afc40f3e9ba716374add171aee7fabc70b5146a510705c6d41b", size = 10603264, upload-time = "2026-03-06T01:56:52.134Z" }, - { url = "https://files.pythonhosted.org/packages/f8/5b/2ecc7a2175243a4bcb72f5298ae41feabbb93b764bb0dc45722f3752c2c2/ty-0.0.21-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:62f7f5b235c4f7876db305c36997aea07b7af29b1a068f373d0e2547e25f32ff", size = 11196428, upload-time = "2026-03-06T01:57:32.94Z" }, - { url = "https://files.pythonhosted.org/packages/37/f5/aff507d6a901f328ef96a298032b0c11aaaf950a146ed7dd3b5bf2cd3acf/ty-0.0.21-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ee8399f7c453a425291e6688efe430cfae7ab0ac4ffd50eba9f872bf878b54f6", size = 10866355, upload-time = "2026-03-06T01:56:57.831Z" }, - { url = "https://files.pythonhosted.org/packages/be/30/822bbcb92d55b65989aa7ed06d9585f28ade9c9447369194ed4b0fb3b5b9/ty-0.0.21-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:210e7568c9f886c4d01308d751949ee714ad7ad9d7d928d2ba90d329dd880367", size = 10738177, upload-time = "2026-03-06T01:57:11.256Z" }, - { url = "https://files.pythonhosted.org/packages/57/cc/46e7991b6469e93ac2c7e533a028983e402485580150ac864c56352a3a82/ty-0.0.21-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:53508e345b11569f78b21ba8e2b4e61df38a9754947fb3cd9f2ef574367338fb", size = 10079158, upload-time = "2026-03-06T01:57:00.516Z" }, - { url = "https://files.pythonhosted.org/packages/15/c2/0bbdadfbd008240f8f1a87dc877433cb3884436097926107ccf06e618199/ty-0.0.21-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:553e43571f4a35604c36cfd07d8b61a5eb7a714e3c67f8c4ff2cf674fefbaef9", size = 10150535, upload-time = "2026-03-06T01:57:08.815Z" }, - { url = "https://files.pythonhosted.org/packages/c5/b5/2dbdb7b57b5362200ef0a39738ebd31331726328336def0143ac097ee59d/ty-0.0.21-py3-none-musllinux_1_2_i686.whl", hash = "sha256:666f6822e3b9200abfa7e95eb0ddd576460adb8d66b550c0ad2c70abc84a2048", size = 10319803, upload-time = "2026-03-06T01:57:19.106Z" }, - { url = "https://files.pythonhosted.org/packages/72/84/70e52c0b7abc7c2086f9876ef454a73b161d3125315536d8d7e911c94ca4/ty-0.0.21-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a0854d008347ce4a5fb351af132f660a390ab2a1163444d075251d43e6f74b9b", size = 10826239, upload-time = "2026-03-06T01:57:21.727Z" }, - { url = "https://files.pythonhosted.org/packages/a1/8a/1f72480fd013bbc6cd1929002abbbcde9a0b08ead6a15154de9d7f7fa37e/ty-0.0.21-py3-none-win32.whl", hash = "sha256:bef3ab4c7b966bcc276a8ac6c11b63ba222d21355b48d471ea782c4104eee4e0", size = 9693196, upload-time = "2026-03-06T01:57:24.126Z" }, - { url = "https://files.pythonhosted.org/packages/8d/f8/1104808b875c26c640e536945753a78562d606bef4e241d9dbf3d92477f6/ty-0.0.21-py3-none-win_amd64.whl", hash = "sha256:a709d576e5bea84b745d43058d8b9cd4f27f74a0b24acb4b0cbb7d3d41e0d050", size = 10668660, upload-time = "2026-03-06T01:56:55.06Z" }, - { url = "https://files.pythonhosted.org/packages/1b/b8/25e0adc404bbf986977657b25318991f93097b49f8aea640d93c0b0db68e/ty-0.0.21-py3-none-win_arm64.whl", hash = "sha256:f72047996598ac20553fb7e21ba5741e3c82dee4e9eadf10d954551a5fe09391", size = 10104161, upload-time = "2026-03-06T01:57:06.072Z" }, +version = "0.0.64" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/aa/14c9965d3b173105692473897cc89c34cd91241368b2044e43167e1c17ff/ty-0.0.64.tar.gz", hash = "sha256:d12ddbb05f15158bb518af619378b385486450def95fb06f8ab98037febe9f2c", size = 6350966, upload-time = "2026-07-27T18:32:45.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/4c/c54937e4ff3fa7b34a99ea3387ec766bf0ad98dc8df8d792e89b388e658e/ty-0.0.64-py3-none-linux_armv6l.whl", hash = "sha256:3830a6675ab43635ced1c4c557f380ac4a49e9414e03975e4e4e8db644c64944", size = 12118357, upload-time = "2026-07-27T18:32:08.702Z" }, + { url = "https://files.pythonhosted.org/packages/ce/aa/a839ee2bc78e943d079e6abe199a97b4eeffb7e5c9a57326d69de452186a/ty-0.0.64-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3ff07d7bc32a2135f58afe57393789a32ea2fed1a66216129a8e535e76043903", size = 11790882, upload-time = "2026-07-27T18:32:10.997Z" }, + { url = "https://files.pythonhosted.org/packages/4c/de/19f14357888a7198438926303753cf749428e3d62e8980ff1e9a72a78402/ty-0.0.64-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4f6d1c7f897cca05d12bacbf1435150d5ffa496099515aa6ed303c8b29e1d0bb", size = 11317394, upload-time = "2026-07-27T18:32:13.162Z" }, + { url = "https://files.pythonhosted.org/packages/08/2f/f54462300535ab99b551eda733177be2eef5dbc2997d3fdb357c4ddd760a/ty-0.0.64-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:138d6c37ad4bf8583aa7a9b29d90954151d7856f9910a03eae3eb34b34c57215", size = 11863042, upload-time = "2026-07-27T18:32:15.307Z" }, + { url = "https://files.pythonhosted.org/packages/5e/95/dbecf745520ebe8bd7b02fc55eee6441c9be312ebf6addce605eb52740dd/ty-0.0.64-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1ed9719d1b7b66fb8efe073d860208a44c40af1f6cd5c2364aa9b323a1e579b4", size = 11910730, upload-time = "2026-07-27T18:32:17.467Z" }, + { url = "https://files.pythonhosted.org/packages/3b/26/12cfd40028e51ceed7b3cb645281c61c02eb64ff9fb0c09231d65c30ff25/ty-0.0.64-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d68b23e5169e2137b5f1de7169ab0cebecab6c8eda374c34c1f6394308f58242", size = 12631936, upload-time = "2026-07-27T18:32:19.533Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d0/65ffc2b0a686347193c6f98e9421a7fc2a96fc3cd0b1001cf7cf284baff9/ty-0.0.64-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f41cb07d89d32626fcaf3ed4d262778fcb28b2628b6ed1e172cd7b18820668d8", size = 13171049, upload-time = "2026-07-27T18:32:22.026Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a4/975a5961842dcd6fa60f0770a102c0bba7509da909ab652919bfcdcd4fc7/ty-0.0.64-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5f24e1504ab9e212f92356b82fe088fdeb3a39f9a2f4ff25e505d2e1d0db9056", size = 12826438, upload-time = "2026-07-27T18:32:24.178Z" }, + { url = "https://files.pythonhosted.org/packages/af/ef/dfb9b7f9bcc032d3b540b0d1f55f532a336e2fb41b1bd539c05ae81a151a/ty-0.0.64-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:86db830cb914bb33bb8247b66ccea58de4496c2391bd42658aba744b439f3290", size = 12440880, upload-time = "2026-07-27T18:32:26.341Z" }, + { url = "https://files.pythonhosted.org/packages/ee/d0/bedac20505e8a8f5501ad73d7d15d8e421a563fef59993909a23036929a4/ty-0.0.64-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:652ee3d6d03bea76cd2fe8949c78bb5970394ebd7c5fd270e9518c0dee1b931d", size = 12782439, upload-time = "2026-07-27T18:32:28.642Z" }, + { url = "https://files.pythonhosted.org/packages/79/d5/795733f13ceff1378f08b3de0c49d0f518df220ed856b0dfac869f3b7c81/ty-0.0.64-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:4838768295774a86e95f9ec5633e739d016adbbb69dcbdc62f3549578a11f624", size = 11814821, upload-time = "2026-07-27T18:32:30.632Z" }, + { url = "https://files.pythonhosted.org/packages/52/3e/9d99cd1e1831003434f508ed9f258a56543194afc3bbe051eba2545fa676/ty-0.0.64-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5a3d700669868599edf39ce5125196682f15a8880cc8c669bc2a83b99984d99b", size = 11928678, upload-time = "2026-07-27T18:32:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3d/448f49a3503fb119a34348a5714bb92001f252fadbbb12d645f2e744b557/ty-0.0.64-py3-none-musllinux_1_2_i686.whl", hash = "sha256:b161f0a82a8e2f2432db3bf7702b4d3924fa9486ba0014f6710a160fc157df0d", size = 12202249, upload-time = "2026-07-27T18:32:34.905Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/75768e562cec990d189dc05807ae72890b20ffbd1e1f43597bb98db63c60/ty-0.0.64-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:39b9dd42908df47c2dc57dda87e656fab97097ffd2618474bbdea986af0d6a9d", size = 12548817, upload-time = "2026-07-27T18:32:36.995Z" }, + { url = "https://files.pythonhosted.org/packages/34/89/44cc276ea6ca0245495014758ffeadde1798fb0b5840c9cf36d8d2ed3250/ty-0.0.64-py3-none-win32.whl", hash = "sha256:d0676ab0e0935795e5843baa28dd5e366dc343d7c0945a996df9eab8e0644885", size = 11545474, upload-time = "2026-07-27T18:32:39.389Z" }, + { url = "https://files.pythonhosted.org/packages/01/7e/d1c8a871a38d17c8f168b9a6975f6247f7660f8334e517656a5e4b4a4858/ty-0.0.64-py3-none-win_amd64.whl", hash = "sha256:dcb9bd31f54097e362b776c26ab4564d4564cdd1355cb883481167a26c03cc3f", size = 12542987, upload-time = "2026-07-27T18:32:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/6d18640d0204cacd69abbaca95ad6a34c6d7e9169e9051d0117b17b827ec/ty-0.0.64-py3-none-win_arm64.whl", hash = "sha256:82cc34c1ad9a8feb6059aef193bebcecc656e548f6fab3d518bcd8b57d198d39", size = 11899263, upload-time = "2026-07-27T18:32:43.366Z" }, ] [[package]] From b0204482ebf68b5dcf661a0f91b8f282a97b97c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Pedro=20Brun?= Date: Wed, 29 Jul 2026 15:55:06 -0300 Subject: [PATCH 2/2] bump ruff to 0.16 and apply format correction --- pyproject.toml | 2 +- src/sap_cloud_sdk/adms/user-guide.md | 13 +- .../agent_decorators/user-guide.md | 6 + src/sap_cloud_sdk/agent_memory/user-guide.md | 8 +- src/sap_cloud_sdk/agentgateway/user-guide.md | 8 +- src/sap_cloud_sdk/aicore/user-guide.md | 97 ++--- src/sap_cloud_sdk/core/auditlog/user-guide.md | 81 ++-- .../core/auditlog_ng/user-guide.md | 8 +- .../core/data_anonymization/user-guide.md | 18 +- .../core/runtime_context/user-guide.md | 39 +- .../core/secret_resolver/user-guide.md | 40 +- .../core/telemetry/user-guide.md | 35 +- src/sap_cloud_sdk/destination/spec/README.md | 16 +- src/sap_cloud_sdk/destination/user-guide.md | 348 ++++++++++++++---- src/sap_cloud_sdk/dms/user-guide.md | 33 +- src/sap_cloud_sdk/extensibility/user-guide.md | 97 ++--- src/sap_cloud_sdk/ias/user-guide.md | 14 +- src/sap_cloud_sdk/objectstore/user-guide.md | 22 +- src/sap_cloud_sdk/print/user-guide.md | 11 +- uv.lock | 66 ++-- 20 files changed, 647 insertions(+), 315 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 980c6e95..19a01bee 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -65,7 +65,7 @@ dev = [ "python-dotenv>=1.0.0", "ty==0.0.64", "cryptography>=46.0.3", - "ruff==0.15.5", + "ruff==0.16.0", "starlette>=0.40.0", "anyio>=3.6.2", "httpx>=0.27.0", diff --git a/src/sap_cloud_sdk/adms/user-guide.md b/src/sap_cloud_sdk/adms/user-guide.md index c550a584..ac4dd076 100644 --- a/src/sap_cloud_sdk/adms/user-guide.md +++ b/src/sap_cloud_sdk/adms/user-guide.md @@ -45,6 +45,7 @@ relation = client.relations.create( # Upload bytes to the presigned URL (outside SDK) import requests + upload_url = relation.document.document_content_upload_urls[0] requests.put(upload_url, data=open("Invoice.pdf", "rb")) ``` @@ -82,6 +83,7 @@ client = create_client(user_jwt=request.headers["Authorization"].split()[1]) ```python from sap_cloud_sdk.adms import create_client, TokenCache + # By default tokens are cached in-process via InMemoryTokenCache. # For multi-instance deployments (Kyma replicas > 1, CF instances > 1), # implement your own TokenCache subclass backed by the shared cache your @@ -91,13 +93,21 @@ class MySharedCache(TokenCache): def set(self, key, token, ttl_seconds): ... def delete(self, key): ... + client = create_client(token_cache=MySharedCache()) ``` ## Async Client ```python -from sap_cloud_sdk.adms import create_async_client, BaseType, CreateDocumentInput, CreateDocumentRelationInput, RelationQueryOptions +from sap_cloud_sdk.adms import ( + create_async_client, + BaseType, + CreateDocumentInput, + CreateDocumentRelationInput, + RelationQueryOptions, +) + async def main(): async with create_async_client() as client: @@ -203,6 +213,7 @@ job = client.jobs.start_zip_download(params) # Poll until terminal state import time + while not job.job_status or not job.job_status.is_terminal(): time.sleep(2) job = client.jobs.get_status(job.job_id) diff --git a/src/sap_cloud_sdk/agent_decorators/user-guide.md b/src/sap_cloud_sdk/agent_decorators/user-guide.md index 1b7000f8..f53ac5b8 100644 --- a/src/sap_cloud_sdk/agent_decorators/user-guide.md +++ b/src/sap_cloud_sdk/agent_decorators/user-guide.md @@ -21,6 +21,7 @@ from sap_cloud_sdk.agent_decorators import ( ```python from sap_cloud_sdk.agent_decorators import prompt_section, agent_model + # Define a prompt with a coded default @prompt_section( key="prompts.system", @@ -30,6 +31,7 @@ from sap_cloud_sdk.agent_decorators import prompt_section, agent_model def system_prompt() -> str: return "You are a helpful assistant." + # Define the model selection @agent_model(key="config.model", label="LLM Model") def model_name() -> str: @@ -45,6 +47,7 @@ Expose a prompt section for editing. ```python from sap_cloud_sdk.agent_decorators import prompt_section + @prompt_section( key="prompts.identity", label="Agent Identity", @@ -62,6 +65,7 @@ Expose a configuration value for editing. ```python from sap_cloud_sdk.agent_decorators import agent_config + @agent_config( key="config.temperature", label="Temperature", @@ -78,6 +82,7 @@ Expose a model selection. The `description` parameter is optional. ```python from sap_cloud_sdk.agent_decorators import agent_model + @agent_model( key="config.model", label="Default Model", @@ -93,6 +98,7 @@ def default_model() -> str: from sap_cloud_sdk.agent_decorators.exceptions import AgentDecoratorError try: + @prompt_section(key="", label="L", description="D") def bad(): return "" diff --git a/src/sap_cloud_sdk/agent_memory/user-guide.md b/src/sap_cloud_sdk/agent_memory/user-guide.md index 94e5cabd..ce184bb2 100644 --- a/src/sap_cloud_sdk/agent_memory/user-guide.md +++ b/src/sap_cloud_sdk/agent_memory/user-guide.md @@ -713,6 +713,7 @@ elif total <= PAGE_SIZE: agent_id="my-agent", invoker_id="user-123", limit=total ) else: + def iter_all_memories(client, agent_id, invoker_id, page_size=PAGE_SIZE): offset = 0 while True: @@ -780,7 +781,8 @@ The default `threshold` of `0.6` may be too strict for your data. Try a lower va ```python results = client.search_memories( - agent_id="my-agent", invoker_id="user-123", + agent_id="my-agent", + invoker_id="user-123", query="user display preferences", threshold=0.3, ) @@ -932,10 +934,10 @@ from sap_cloud_sdk.agent_memory.factory.langgraph_checkpoint import create_check key="config.checkpointer.ttl_seconds", label="Thread TTL (seconds)", description="Evict inactive conversation threads after this period of " - "inactivity. Set to 0 to disable eviction.", + "inactivity. Set to 0 to disable eviction.", ) def thread_ttl_seconds() -> int: - return 3600 # 1 hour + return 3600 # 1 hour class MyAgent: diff --git a/src/sap_cloud_sdk/agentgateway/user-guide.md b/src/sap_cloud_sdk/agentgateway/user-guide.md index c9eec51d..0ff6ddbb 100644 --- a/src/sap_cloud_sdk/agentgateway/user-guide.md +++ b/src/sap_cloud_sdk/agentgateway/user-guide.md @@ -250,7 +250,7 @@ from sap_cloud_sdk.agentgateway import AgentCardFilter AgentCardFilter( agent_names=[], # agent card names to include (matched against card JSON `name`); empty = no filter - ord_ids=[], # ORD IDs to include (extracted from fragment URL); empty = no filter + ord_ids=[], # ORD IDs to include (extracted from fragment URL); empty = no filter ) ``` @@ -261,12 +261,14 @@ Both fields default to empty lists. Filters are applied with AND semantics: if b ```python @dataclass class Agent: - ord_id: str # ORD ID from fragment ordId property + ord_id: str # ORD ID from fragment ordId property agent_card: AgentCard + @dataclass class AgentCard: - raw: dict # full parsed JSON from /.well-known/agent-card.json + raw: dict # full parsed JSON from /.well-known/agent-card.json + @dataclass class MCPTool: diff --git a/src/sap_cloud_sdk/aicore/user-guide.md b/src/sap_cloud_sdk/aicore/user-guide.md index b015caf5..35712d2e 100644 --- a/src/sap_cloud_sdk/aicore/user-guide.md +++ b/src/sap_cloud_sdk/aicore/user-guide.md @@ -30,8 +30,7 @@ set_aicore_config() from litellm import completion response = completion( - model="sap/gpt-4", - messages=[{"role": "user", "content": "Hello!"}] + model="sap/gpt-4", messages=[{"role": "user", "content": "Hello!"}] ) ``` @@ -115,25 +114,31 @@ from sap_cloud_sdk.aicore import ( set_filtering, ) -set_filtering(ContentFiltering( - input_filtering=InputFiltering(filters=[ - AzureContentFilter( - hate=Severity.STRICT, - violence=Severity.STRICT, - sexual=Severity.STRICT, - self_harm=Severity.STRICT, - prompt_shield=True, +set_filtering( + ContentFiltering( + input_filtering=InputFiltering( + filters=[ + AzureContentFilter( + hate=Severity.STRICT, + violence=Severity.STRICT, + sexual=Severity.STRICT, + self_harm=Severity.STRICT, + prompt_shield=True, + ), + ] ), - ]), - output_filtering=OutputFiltering(filters=[ - AzureContentFilter( - hate=Severity.MEDIUM, - violence=Severity.MEDIUM, - sexual=Severity.MEDIUM, - self_harm=Severity.MEDIUM, + output_filtering=OutputFiltering( + filters=[ + AzureContentFilter( + hate=Severity.MEDIUM, + violence=Severity.MEDIUM, + sexual=Severity.MEDIUM, + self_harm=Severity.MEDIUM, + ), + ] ), - ]), -)) + ) +) ``` To re-apply env-based config (e.g. after changing `AICORE_FILTER_*`): @@ -161,12 +166,16 @@ from sap_cloud_sdk.aicore import ( set_filtering, ) -set_filtering(ContentFiltering( - input_filtering=InputFiltering(filters=[ - AzureContentFilter(prompt_shield=True), - LlamaGuard38bFilter(hate=True, violent_crimes=True), - ]), -)) +set_filtering( + ContentFiltering( + input_filtering=InputFiltering( + filters=[ + AzureContentFilter(prompt_shield=True), + LlamaGuard38bFilter(hate=True, violent_crimes=True), + ] + ), + ) +) ``` `LlamaGuard38bFilter` takes 14 boolean category toggles (`hate`, @@ -239,15 +248,23 @@ set_filtering(hate=0, violence=0) # After (aicore namespace with class API): from sap_cloud_sdk.aicore import ( - AzureContentFilter, ContentFiltering, InputFiltering, - Severity, ContentFilteredError, set_filtering, + AzureContentFilter, + ContentFiltering, + InputFiltering, + Severity, + ContentFilteredError, + set_filtering, ) -set_filtering(ContentFiltering( - input_filtering=InputFiltering(filters=[ - AzureContentFilter(hate=Severity.STRICT, violence=Severity.STRICT), - ]), -)) +set_filtering( + ContentFiltering( + input_filtering=InputFiltering( + filters=[ + AzureContentFilter(hate=Severity.STRICT, violence=Severity.STRICT), + ] + ), + ) +) ``` Env vars also renamed: `ORCH_FILTER_*` → `AICORE_FILTER_*`. The @@ -283,8 +300,8 @@ response = completion( model="sap/gpt-4", messages=[ {"role": "system", "content": "You are a helpful assistant."}, - {"role": "user", "content": "What is SAP AI Core?"} - ] + {"role": "user", "content": "What is SAP AI Core?"}, + ], ) print(response.choices[0].message.content) @@ -301,7 +318,7 @@ set_aicore_config() response = completion( model="sap/gpt-4", messages=[{"role": "user", "content": "Tell me a story"}], - stream=True + stream=True, ) for chunk in response: @@ -319,14 +336,12 @@ set_aicore_config() # Chat completion chat_response = completion( - model="sap/gpt-4", - messages=[{"role": "user", "content": "Hello!"}] + model="sap/gpt-4", messages=[{"role": "user", "content": "Hello!"}] ) # Text embeddings embedding_response = embedding( - model="sap/text-embedding-ada-002", - input=["Hello world", "How are you?"] + model="sap/text-embedding-ada-002", input=["Hello world", "How are you?"] ) ``` @@ -356,10 +371,10 @@ try: model="sap/gpt-4", messages=[ {"role": "system", "content": "You are a helpful AI assistant."}, - {"role": "user", "content": "Explain SAP AI Core in one sentence."} + {"role": "user", "content": "Explain SAP AI Core in one sentence."}, ], temperature=0.7, - max_tokens=100 + max_tokens=100, ) print(f"Response: {response.choices[0].message.content}") diff --git a/src/sap_cloud_sdk/core/auditlog/user-guide.md b/src/sap_cloud_sdk/core/auditlog/user-guide.md index 84241bb5..abd83b67 100644 --- a/src/sap_cloud_sdk/core/auditlog/user-guide.md +++ b/src/sap_cloud_sdk/core/auditlog/user-guide.md @@ -22,7 +22,7 @@ from sap_cloud_sdk.core.auditlog import ( Tenant, SecurityEventAttribute, DataAccessAttribute, - ChangeAttribute + ChangeAttribute, ) ``` @@ -45,10 +45,7 @@ client = create_client() # Create and log a security event security_event = SecurityEvent( - data="User login attempt", - success=True, - user="john.doe", - tenant=Tenant.PROVIDER + data="User login attempt", success=True, user="john.doe", tenant=Tenant.PROVIDER ) client.log(security_event) @@ -65,7 +62,7 @@ config = AuditLogConfig( service_url="https://api.auditlog.cf.example.com/audit-log/oauth2/v2", oauth_url="https://example.authentication.com/oauth/token", client_id="your-client-id", - client_secret="your-client-secret" + client_secret="your-client-secret", ) client = create_client(config=config) @@ -98,7 +95,10 @@ Security events record authentication attempts, authorization failures, and othe ```python from sap_cloud_sdk.core.auditlog import ( - create_client, SecurityEvent, SecurityEventAttribute, Tenant + create_client, + SecurityEvent, + SecurityEventAttribute, + Tenant, ) client = create_client() @@ -112,8 +112,8 @@ security_event = SecurityEvent( ip="192.168.1.100", attributes=[ SecurityEventAttribute("login_method", "password"), - SecurityEventAttribute("failure_reason", "invalid_credentials") - ] + SecurityEventAttribute("failure_reason", "invalid_credentials"), + ], ) client.log(security_event) @@ -130,7 +130,10 @@ Data access events record when personal or sensitive data is read or accessed. T ```python from sap_cloud_sdk.core.auditlog import ( - create_client, DataAccessEvent, DataAccessAttribute, Tenant + create_client, + DataAccessEvent, + DataAccessAttribute, + Tenant, ) client = create_client() @@ -144,11 +147,11 @@ data_access = DataAccessEvent( subject_role="end_user", attributes=[ DataAccessAttribute("email", successful=True), - DataAccessAttribute("phone_number", successful=True) + DataAccessAttribute("phone_number", successful=True), ], user="service-account", tenant=Tenant.PROVIDER, - identity_provider="OAuth2" + identity_provider="OAuth2", ) client.log(data_access) @@ -169,7 +172,10 @@ Data modification events record when personal or sensitive data is created, upda ```python from sap_cloud_sdk.core.auditlog import ( - create_client, DataModificationEvent, ChangeAttribute, Tenant + create_client, + DataModificationEvent, + ChangeAttribute, + Tenant, ) client = create_client() @@ -183,10 +189,10 @@ data_modification = DataModificationEvent( subject_role="customer", attributes=[ ChangeAttribute("email", "new@example.com", "old@example.com"), - ChangeAttribute("phone", "+0987654321", "+1234567890") + ChangeAttribute("phone", "+0987654321", "+1234567890"), ], user="admin-user", - tenant=Tenant.PROVIDER + tenant=Tenant.PROVIDER, ) client.log(data_modification) @@ -207,7 +213,10 @@ Configuration change events record when system configurations, settings, or othe ```python from sap_cloud_sdk.core.auditlog import ( - create_client, ConfigurationChangeEvent, ChangeAttribute, Tenant + create_client, + ConfigurationChangeEvent, + ChangeAttribute, + Tenant, ) client = create_client() @@ -218,11 +227,11 @@ config_change = ConfigurationChangeEvent( object_id={"component": "authentication", "setting": "timeout"}, attributes=[ ChangeAttribute("session_timeout", "60", "30"), - ChangeAttribute("max_attempts", "5", "3") + ChangeAttribute("max_attempts", "5", "3"), ], user="system-admin", tenant=Tenant.PROVIDER, - id="config-change-001" + id="config-change-001", ) client.log(config_change) @@ -241,7 +250,10 @@ Data deletion events are specialized for tracking when personal or sensitive dat ```python from sap_cloud_sdk.core.auditlog import ( - create_client, DataDeletionEvent, DeletedAttribute, Tenant + create_client, + DataDeletionEvent, + DeletedAttribute, + Tenant, ) client = create_client() @@ -256,10 +268,10 @@ data_deletion = DataDeletionEvent( attributes=[ DeletedAttribute("email", "deleted@example.com"), DeletedAttribute("phone", "+1234567890"), - DeletedAttribute("personal_notes") + DeletedAttribute("personal_notes"), ], user="admin-user", - tenant=Tenant.PROVIDER + tenant=Tenant.PROVIDER, ) client.log(data_deletion) @@ -280,7 +292,10 @@ Configuration deletion events track when system configurations or settings are r ```python from sap_cloud_sdk.core.auditlog import ( - create_client, ConfigurationDeletionEvent, DeletedAttribute, Tenant + create_client, + ConfigurationDeletionEvent, + DeletedAttribute, + Tenant, ) client = create_client() @@ -292,11 +307,11 @@ config_deletion = ConfigurationDeletionEvent( attributes=[ DeletedAttribute("enabled", "true"), DeletedAttribute("rollout_percentage", "50"), - DeletedAttribute("target_users") + DeletedAttribute("target_users"), ], user="system-admin", tenant=Tenant.PROVIDER, - id="config-deletion-001" + id="config-deletion-001", ) client.log(config_deletion) @@ -321,8 +336,8 @@ security_event = SecurityEvent( "request_id": "req-123", "correlation_id": "corr-456", "source_ip": "10.0.0.1", - "user_agent": "MyApp/1.0" - } + "user_agent": "MyApp/1.0", + }, ) client.log(security_event) @@ -346,10 +361,7 @@ from sap_cloud_sdk.core.auditlog.exceptions import AuditLogError client = create_client() -security_event = SecurityEvent( - data="Login attempt", - success=False -) +security_event = SecurityEvent(data="Login attempt", success=False) try: client.log(security_event) @@ -367,7 +379,10 @@ The audit client supports batch logging for efficiency when logging multiple eve ```python from sap_cloud_sdk.core.auditlog import ( - create_client, SecurityEvent, DataAccessEvent, DataAccessAttribute + create_client, + SecurityEvent, + DataAccessEvent, + DataAccessAttribute, ) client = create_client() @@ -382,8 +397,8 @@ events = [ subject_type="user", subject_id={"id": "456"}, subject_role="user", - attributes=[DataAccessAttribute("email", successful=True)] - ) + attributes=[DataAccessAttribute("email", successful=True)], + ), ] # Log all events in a batch diff --git a/src/sap_cloud_sdk/core/auditlog_ng/user-guide.md b/src/sap_cloud_sdk/core/auditlog_ng/user-guide.md index 771bba73..3052f85d 100644 --- a/src/sap_cloud_sdk/core/auditlog_ng/user-guide.md +++ b/src/sap_cloud_sdk/core/auditlog_ng/user-guide.md @@ -100,7 +100,9 @@ The destination `url` is used as the OTLP endpoint. The lookup is always perform ```python from sap_cloud_sdk.core.auditlog_ng import create_client, AuditLogNGConfig -from sap_cloud_sdk.core.auditlog_ng.gen.sap.auditlog.auditevent.v2 import auditevent_pb2 as pb +from sap_cloud_sdk.core.auditlog_ng.gen.sap.auditlog.auditevent.v2 import ( + auditevent_pb2 as pb, +) ``` ### Step 2: Initialize the Client @@ -202,7 +204,9 @@ client.close() ```python from sap_cloud_sdk.core.auditlog_ng import create_client -from sap_cloud_sdk.core.auditlog_ng.gen.sap.auditlog.auditevent.v2 import auditevent_pb2 as pb +from sap_cloud_sdk.core.auditlog_ng.gen.sap.auditlog.auditevent.v2 import ( + auditevent_pb2 as pb, +) from datetime import datetime, timezone diff --git a/src/sap_cloud_sdk/core/data_anonymization/user-guide.md b/src/sap_cloud_sdk/core/data_anonymization/user-guide.md index fd9935d6..5548c30c 100644 --- a/src/sap_cloud_sdk/core/data_anonymization/user-guide.md +++ b/src/sap_cloud_sdk/core/data_anonymization/user-guide.md @@ -53,9 +53,7 @@ client = create_client() # Anonymize – irreversible PII removal result = client.anonymize_text( - AnonymizeTextRequest( - text="Please contact John Doe at john@example.com" - ) + AnonymizeTextRequest(text="Please contact John Doe at john@example.com") ) assert result.result is not None @@ -73,7 +71,8 @@ Pass a `DataAnonymizationConfig` directly when the base64-encoded client certifi ```python from sap_cloud_sdk.core.data_anonymization import ( - create_client, DataAnonymizationConfig, + create_client, + DataAnonymizationConfig, ) config = DataAnonymizationConfig( @@ -130,7 +129,8 @@ Irreversible. All detected PII entities are replaced with category placeholders ```python from sap_cloud_sdk.core.data_anonymization import ( - create_client, AnonymizeTextRequest, + create_client, + AnonymizeTextRequest, ) client = create_client() @@ -139,7 +139,7 @@ request = AnonymizeTextRequest( text="Employee Alice Smith (alice@corp.com, +1-555-0100) submitted the report.", entities=["profile-person", "profile-email", "profile-phone"], allowlist="Alice Smith", - enable_default_allowlist=False + enable_default_allowlist=False, ) result = client.anonymize_text(request) @@ -163,7 +163,8 @@ Reversible. Each PII entity is replaced with a consistent token. The mapping bet ```python from sap_cloud_sdk.core.data_anonymization import ( - create_client, PseudonymizeTextRequest, + create_client, + PseudonymizeTextRequest, ) client = create_client() @@ -201,8 +202,7 @@ from sap_cloud_sdk.core.data_anonymization import ( client = create_client() request = AnonymizeFileRequest( - file_path="sample.pdf", - entities=["profile-person", "profile-email"] + file_path="sample.pdf", entities=["profile-person", "profile-email"] ) result = client.anonymize_file(request) diff --git a/src/sap_cloud_sdk/core/runtime_context/user-guide.md b/src/sap_cloud_sdk/core/runtime_context/user-guide.md index 0bb989e8..2508021f 100644 --- a/src/sap_cloud_sdk/core/runtime_context/user-guide.md +++ b/src/sap_cloud_sdk/core/runtime_context/user-guide.md @@ -38,12 +38,17 @@ By default `bootstrap` registers `IASContextProvider` (reads IAS JWT), ### 2. Read context anywhere ```python -from sap_cloud_sdk.core.runtime_context import get_context, TENANT_ID, USER_ID, TRIGGER_TYPE +from sap_cloud_sdk.core.runtime_context import ( + get_context, + TENANT_ID, + USER_ID, + TRIGGER_TYPE, +) ctx = get_context() -ctx.get(TENANT_ID) # -> "abc-123" or None -ctx.get(USER_ID) # -> "user-uuid" or None -ctx.get(TRIGGER_TYPE) # -> "ui5" or None +ctx.get(TENANT_ID) # -> "abc-123" or None +ctx.get(USER_ID) # -> "user-uuid" or None +ctx.get(TRIGGER_TYPE) # -> "ui5" or None ``` --- @@ -96,11 +101,15 @@ metadata or a message queue envelope — as long as the adapter populates ```python from sap_cloud_sdk.core.runtime_context import ( - ContextKey, ContextProvider, RuntimeContext, RequestEnvelope + ContextKey, + ContextProvider, + RuntimeContext, + RequestEnvelope, ) CORRELATION_ID = ContextKey[str]("correlation_id") + class CorrelationIdProvider(ContextProvider): def extract(self, envelope: RequestEnvelope) -> RuntimeContext: value = envelope.headers.get("x-correlation-id") @@ -116,7 +125,15 @@ from sap_cloud_sdk.core.runtime_context import ( DWCContextProvider, ) -bootstrap(app, providers=[IASContextProvider(), SAPTriggerContextProvider(), DWCContextProvider(), CorrelationIdProvider()]) +bootstrap( + app, + providers=[ + IASContextProvider(), + SAPTriggerContextProvider(), + DWCContextProvider(), + CorrelationIdProvider(), + ], +) ``` ### Merging @@ -143,17 +160,25 @@ editing `bootstrap`. ### Adding a new framework or invocation source ```python -from sap_cloud_sdk.core.runtime_context import ContextProvider, FrameworkAdapter, register +from sap_cloud_sdk.core.runtime_context import ( + ContextProvider, + FrameworkAdapter, + register, +) + class FlaskContextAdapter(FrameworkAdapter): def _matches(self, app) -> bool: from flask import Flask + return isinstance(app, Flask) def attach(self, app, providers: list[ContextProvider]) -> None: from my_flask_middleware import FlaskContextMiddleware + app.before_request(FlaskContextMiddleware(providers).handle) + register(FlaskContextAdapter()) ``` diff --git a/src/sap_cloud_sdk/core/secret_resolver/user-guide.md b/src/sap_cloud_sdk/core/secret_resolver/user-guide.md index c7950738..35f25fc2 100644 --- a/src/sap_cloud_sdk/core/secret_resolver/user-guide.md +++ b/src/sap_cloud_sdk/core/secret_resolver/user-guide.md @@ -23,6 +23,7 @@ The Secret Resolver loads configuration into dataclass objects using a hierarchi from dataclasses import dataclass from sap_cloud_sdk.secret_resolver import read_from_mount_and_fallback_to_env_var + @dataclass class DatabaseConfig: host: str = "" @@ -30,14 +31,15 @@ class DatabaseConfig: username: str = "" password: str = "" + # Load configuration config = DatabaseConfig() read_from_mount_and_fallback_to_env_var( - base_volume_mount="/etc/secrets", # Base mount path - base_var_name="DB", # Environment variable prefix - module="database", # Module/service name - instance="primary", # Instance name - target=config # Target dataclass instance + base_volume_mount="/etc/secrets", # Base mount path + base_var_name="DB", # Environment variable prefix + module="database", # Module/service name + instance="primary", # Instance name + target=config, # Target dataclass instance ) print(f"Database: {config.username}@{config.host}:{config.port}") @@ -114,6 +116,7 @@ This is how the ObjectStore module uses the Secret Resolver internally: from dataclasses import dataclass from sap_cloud_sdk.secret_resolver import read_from_mount_and_fallback_to_env_var + @dataclass class ObjectStoreConfig: access_key_id: str = "" @@ -121,6 +124,7 @@ class ObjectStoreConfig: bucket: str = "" host: str = "" + # Load ObjectStore credentials config = ObjectStoreConfig() read_from_mount_and_fallback_to_env_var( @@ -128,7 +132,7 @@ read_from_mount_and_fallback_to_env_var( base_var_name="OBJECTSTORE", module="objectstore", instance="credentials", - target=config + target=config, ) ``` @@ -157,14 +161,16 @@ export OBJECTSTORE_OBJECTSTORE_CREDENTIALS_HOST="s3.amazonaws.com" from dataclasses import dataclass from sap_cloud_sdk.secret_resolver import read_from_mount_and_fallback_to_env_var + @dataclass class DatabaseConfig: host: str = "" - port: str = "5432" # Default value + port: str = "5432" # Default value database: str = "" username: str = "" password: str = "" - ssl_mode: str = "require" # Default value + ssl_mode: str = "require" # Default value + # Load primary database config primary_db = DatabaseConfig() @@ -173,7 +179,7 @@ read_from_mount_and_fallback_to_env_var( base_var_name="APP", module="database", instance="primary", - target=primary_db + target=primary_db, ) # Load read replica config @@ -183,7 +189,7 @@ read_from_mount_and_fallback_to_env_var( base_var_name="APP", module="database", instance="replica", - target=replica_db + target=replica_db, ) print(f"Primary DB: {primary_db.username}@{primary_db.host}") @@ -196,6 +202,7 @@ print(f"Replica DB: {replica_db.username}@{replica_db.host}") from dataclasses import dataclass from sap_cloud_sdk.secret_resolver import read_from_mount_and_fallback_to_env_var + @dataclass class ApiConfig: base_url: str = "" @@ -203,6 +210,7 @@ class ApiConfig: timeout: str = "30" retries: str = "3" + # Load external API configuration api_config = ApiConfig() read_from_mount_and_fallback_to_env_var( @@ -210,7 +218,7 @@ read_from_mount_and_fallback_to_env_var( base_var_name="EXTERNAL", module="payment", instance="stripe", - target=api_config + target=api_config, ) # Convert string values to appropriate types @@ -235,12 +243,14 @@ The Secret Resolver handles missing secrets gracefully by leaving default values from dataclasses import dataclass from sap_cloud_sdk.secret_resolver import read_from_mount_and_fallback_to_env_var + @dataclass class ServiceConfig: api_key: str = "" - timeout: str = "30" # Default value - retries: str = "3" # Default value - debug: str = "false" # Default value + timeout: str = "30" # Default value + retries: str = "3" # Default value + debug: str = "false" # Default value + config = ServiceConfig() @@ -250,7 +260,7 @@ read_from_mount_and_fallback_to_env_var( base_var_name="API", module="external", instance="service", - target=config + target=config, ) # Check if required values were loaded diff --git a/src/sap_cloud_sdk/core/telemetry/user-guide.md b/src/sap_cloud_sdk/core/telemetry/user-guide.md index 62863ce9..c593920d 100644 --- a/src/sap_cloud_sdk/core/telemetry/user-guide.md +++ b/src/sap_cloud_sdk/core/telemetry/user-guide.md @@ -36,7 +36,9 @@ Wrap your LLM calls to add the context autoinstrumentation can't provide: ```python from sap_cloud_sdk.core.telemetry import invoke_agent_span -with invoke_agent_span(provider="openai", agent_name="SupportBot", conversation_id="conv-123"): +with invoke_agent_span( + provider="openai", agent_name="SupportBot", conversation_id="conv-123" +): # autoinstrumented LLM call is a child of this span response = client.chat.completions.create(...) ``` @@ -46,6 +48,7 @@ with invoke_agent_span(provider="openai", agent_name="SupportBot", conversation_ ```python from sap_cloud_sdk.core.telemetry import set_tenant_id + def handle_request(request): set_tenant_id(extract_tenant_from_jwt(request)) ``` @@ -85,7 +88,9 @@ For operations following [OpenTelemetry GenAI conventions](https://opentelemetry from sap_cloud_sdk.core.telemetry import chat_span, execute_tool_span, invoke_agent_span # Agent invocation — top-level parent span for an agent turn -with invoke_agent_span(provider="openai", agent_name="SupportBot", conversation_id="cid"): +with invoke_agent_span( + provider="openai", agent_name="SupportBot", conversation_id="cid" +): response = client.beta.threads.runs.create(...) # LLM chat call — use when autoinstrumentation is not available @@ -93,7 +98,9 @@ with chat_span(model="gpt-4", provider="openai", conversation_id="cid") as span: response = client.chat.completions.create(...) # Tool execution -with execute_tool_span(tool_name="get_weather", tool_type="mcp", tool_description="weather mcp server"): +with execute_tool_span( + tool_name="get_weather", tool_type="mcp", tool_description="weather mcp server" +): result = call_weather_api(location) ``` @@ -133,8 +140,8 @@ with extension_context( Available extension types: ```python -ExtensionType.TOOL # MCP tool call (default) -ExtensionType.INSTRUCTION # Instruction/prompt injection +ExtensionType.TOOL # MCP tool call (default) +ExtensionType.INSTRUCTION # Instruction/prompt injection ``` In downstream services, read the propagated context: @@ -144,9 +151,9 @@ from sap_cloud_sdk.core.telemetry import get_extension_context ext_ctx = get_extension_context() if ext_ctx: - print(ext_ctx["capability_id"]) # "default" - print(ext_ctx["extension_name"]) # "ServiceNow Extension" - print(ext_ctx["extension_type"]) # "tool" + print(ext_ctx["capability_id"]) # "default" + print(ext_ctx["extension_name"]) # "ServiceNow Extension" + print(ext_ctx["extension_type"]) # "tool" ``` The extension baggage span processor (registered automatically by `auto_instrument()`) @@ -202,7 +209,7 @@ with invoke_agent_span( provider="openai", agent_name="SupportBot", attributes={"user.id": "u-456"}, - propagate=True + propagate=True, ): # child spans automatically receive user.id with execute_tool_span("search"): @@ -237,15 +244,14 @@ auto_instrument() from litellm import completion + async def handle_request(query: str, user_id: str): set_tenant_id("bh7sjh...") # Parent span carries business context for the whole agent turn. # Autoinstrumentation creates the child LLM span automatically. with invoke_agent_span( - provider="openai", - agent_name="SupportBot", - attributes={"user.id": user_id} + provider="openai", agent_name="SupportBot", attributes={"user.id": user_id} ): documents = await retrieve_knowledge_base(query) add_span_attribute("documents.retrieved", len(documents)) @@ -254,8 +260,8 @@ async def handle_request(query: str, user_id: str): model="gpt-4", messages=[ {"role": "system", "content": f"Context: {documents}"}, - {"role": "user", "content": query} - ] + {"role": "user", "content": query}, + ], ) return response @@ -274,6 +280,7 @@ You can write your own by subclassing `TelemetryMiddleware`: ```python from sap_cloud_sdk.core.telemetry.middleware.base import TelemetryMiddleware + class MyMiddleware(TelemetryMiddleware): def register(self) -> None: # hook into your framework here diff --git a/src/sap_cloud_sdk/destination/spec/README.md b/src/sap_cloud_sdk/destination/spec/README.md index 8fbb499e..aedc7e31 100644 --- a/src/sap_cloud_sdk/destination/spec/README.md +++ b/src/sap_cloud_sdk/destination/spec/README.md @@ -132,10 +132,10 @@ from sap_cloud_sdk.destination._models import ListOptions # Create filter with pagination filter_opts = ListOptions( - page=1, # Page number (1-based) - page_size=10, # Number of items per page - page_count=True, # Request total page count in response headers - entity_count=True # Request total entity count in response headers + page=1, # Page number (1-based) + page_size=10, # Number of items per page + page_count=True, # Request total page count in response headers + entity_count=True, # Request total entity count in response headers ) # List destinations with pagination @@ -169,11 +169,7 @@ filter_opts = ListOptions(filter_names=["dest1", "dest2", "dest3"]) destinations = client.list_instance_destinations(filter=filter_opts) # Combine filter with pagination -filter_opts = ListOptions( - filter_names=["dest1", "dest2"], - page=1, - page_size=10 -) +filter_opts = ListOptions(filter_names=["dest1", "dest2"], page=1, page_size=10) destinations = client.list_instance_destinations(filter=filter_opts) ``` @@ -218,7 +214,7 @@ try: dest = client.get_subaccount_destination( "my-dest", access_strategy=AccessStrategy.SUBSCRIBER_ONLY, - tenant=None # Error: tenant required for subscriber access + tenant=None, # Error: tenant required for subscriber access ) except DestinationOperationError as e: print(f"Operation error: {e}") diff --git a/src/sap_cloud_sdk/destination/user-guide.md b/src/sap_cloud_sdk/destination/user-guide.md index 055258de..5039d07c 100644 --- a/src/sap_cloud_sdk/destination/user-guide.md +++ b/src/sap_cloud_sdk/destination/user-guide.md @@ -24,7 +24,9 @@ fragment_client = create_fragment_client(instance="default") certificate_client = create_certificate_client(instance="default") # Instance-level read -dest = client.get_instance_destination("my-destination") # deprecated: use get_destination() +dest = client.get_instance_destination( + "my-destination" +) # deprecated: use get_destination() fragment = fragment_client.get_instance_fragment("my-fragment") cert = certificate_client.get_instance_certificate("my-cert") @@ -39,20 +41,46 @@ fragments = fragment_client.list_instance_fragments(tenant="tenant-subdomain") certificates = certificate_client.list_instance_certificates(tenant="tenant-subdomain") # Subaccount-level read: provider only (no tenant required) -dest = client.get_subaccount_destination("my-destination", access_strategy=AccessStrategy.PROVIDER_ONLY) # deprecated: use get_destination() -fragment = fragment_client.get_subaccount_fragment("my-fragment", access_strategy=AccessStrategy.PROVIDER_ONLY) -cert = certificate_client.get_subaccount_certificate("my-cert", access_strategy=AccessStrategy.PROVIDER_ONLY) +dest = client.get_subaccount_destination( + "my-destination", access_strategy=AccessStrategy.PROVIDER_ONLY +) # deprecated: use get_destination() +fragment = fragment_client.get_subaccount_fragment( + "my-fragment", access_strategy=AccessStrategy.PROVIDER_ONLY +) +cert = certificate_client.get_subaccount_certificate( + "my-cert", access_strategy=AccessStrategy.PROVIDER_ONLY +) # Subaccount-level read: subscriber-first (tenant required), fallback to provider -dest = client.get_subaccount_destination("my-destination", access_strategy=AccessStrategy.SUBSCRIBER_FIRST, tenant="tenant-subdomain") # deprecated: use get_destination() -fragment = fragment_client.get_subaccount_fragment("my-fragment", access_strategy=AccessStrategy.SUBSCRIBER_FIRST, tenant="tenant-subdomain") -cert = certificate_client.get_subaccount_certificate("my-cert", access_strategy=AccessStrategy.SUBSCRIBER_FIRST, tenant="tenant-subdomain") +dest = client.get_subaccount_destination( + "my-destination", + access_strategy=AccessStrategy.SUBSCRIBER_FIRST, + tenant="tenant-subdomain", +) # deprecated: use get_destination() +fragment = fragment_client.get_subaccount_fragment( + "my-fragment", + access_strategy=AccessStrategy.SUBSCRIBER_FIRST, + tenant="tenant-subdomain", +) +cert = certificate_client.get_subaccount_certificate( + "my-cert", + access_strategy=AccessStrategy.SUBSCRIBER_FIRST, + tenant="tenant-subdomain", +) # Fragment write operations with tenant (subscriber context) -new_fragment = Fragment(name="my-fragment", properties={"URL": "https://api.example.com"}) -fragment_client.create_fragment(new_fragment, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") -fragment_client.update_fragment(new_fragment, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") -fragment_client.delete_fragment("my-fragment", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") +new_fragment = Fragment( + name="my-fragment", properties={"URL": "https://api.example.com"} +) +fragment_client.create_fragment( + new_fragment, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) +fragment_client.update_fragment( + new_fragment, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) +fragment_client.delete_fragment( + "my-fragment", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) # Fragment write operations without tenant (provider context) fragment_client.create_fragment(new_fragment, level=Level.SUB_ACCOUNT) @@ -73,10 +101,17 @@ client.delete_destination("my-dest", level=Level.SUB_ACCOUNT) # Certificate write operations with tenant (subscriber context) from sap_cloud_sdk.destination import create_certificate_client from sap_cloud_sdk.destination._models import Certificate + new_cert = Certificate(name="my-cert.pem", content="base64-encoded-content", type="PEM") -certificate_client.create_certificate(new_cert, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") -certificate_client.update_certificate(new_cert, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") -certificate_client.delete_certificate("my-cert.pem", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") +certificate_client.create_certificate( + new_cert, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) +certificate_client.update_certificate( + new_cert, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) +certificate_client.delete_certificate( + "my-cert.pem", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) # Certificate write operations without tenant (provider context) certificate_client.create_certificate(new_cert, level=Level.SUB_ACCOUNT) @@ -85,18 +120,34 @@ certificate_client.delete_certificate("my-cert.pem", level=Level.SUB_ACCOUNT) # Label management with tenant (subscriber context) from sap_cloud_sdk.destination import Label, PatchLabels + labels = [Label(key="env", values=["prod", "eu"])] -client.update_destination_labels("my-dest", labels, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") -client.patch_destination_labels("my-dest", PatchLabels(action="ADD", labels=labels), level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") -retrieved = client.get_destination_labels("my-dest", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") +client.update_destination_labels( + "my-dest", labels, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) +client.patch_destination_labels( + "my-dest", + PatchLabels(action="ADD", labels=labels), + level=Level.SUB_ACCOUNT, + tenant="tenant-subdomain", +) +retrieved = client.get_destination_labels( + "my-dest", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) -fragment_client.update_fragment_labels("my-fragment", labels, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") -certificate_client.get_certificate_labels("my-cert.pem", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") +fragment_client.update_fragment_labels( + "my-fragment", labels, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) +certificate_client.get_certificate_labels( + "my-cert.pem", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) # Label management without tenant (provider context) client.update_destination_labels("my-dest", labels, level=Level.SUB_ACCOUNT) -client.patch_destination_labels("my-dest", PatchLabels(action="DELETE", labels=labels), level=Level.SUB_ACCOUNT) +client.patch_destination_labels( + "my-dest", PatchLabels(action="DELETE", labels=labels), level=Level.SUB_ACCOUNT +) ``` ## Concepts @@ -126,21 +177,79 @@ The client produced by `create_client()` exposes the following operations: ```python class DestinationClient: # V1 Admin API - Read operations for destinations - def get_instance_destination(self, name: str, proxy_enabled: Optional[bool] = None) -> Optional[Destination | TransparentProxyDestination]: ... # deprecated: use get_destination() - def get_subaccount_destination(self, name: str, access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, tenant: Optional[str] = None, proxy_enabled: Optional[bool] = None) -> Optional[Destination | TransparentProxyDestination]: ... # deprecated: use get_destination() - def list_instance_destinations(self, tenant: Optional[str] = None, filter: Optional[ListOptions] = None) -> PagedResult[Destination]: ... - def list_subaccount_destinations(self, access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, tenant: Optional[str] = None, filter: Optional[ListOptions] = None) -> PagedResult[Destination]: ... + def get_instance_destination( + self, name: str, proxy_enabled: Optional[bool] = None + ) -> Optional[ + Destination | TransparentProxyDestination + ]: ... # deprecated: use get_destination() + def get_subaccount_destination( + self, + name: str, + access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, + tenant: Optional[str] = None, + proxy_enabled: Optional[bool] = None, + ) -> Optional[ + Destination | TransparentProxyDestination + ]: ... # deprecated: use get_destination() + def list_instance_destinations( + self, tenant: Optional[str] = None, filter: Optional[ListOptions] = None + ) -> PagedResult[Destination]: ... + def list_subaccount_destinations( + self, + access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, + tenant: Optional[str] = None, + filter: Optional[ListOptions] = None, + ) -> PagedResult[Destination]: ... # V1 Admin API - Write operations - def create_destination(self, dest: Destination, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def update_destination(self, dest: Destination, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def delete_destination(self, name: str, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def get_destination_labels(self, name: str, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> List[Label]: ... - def update_destination_labels(self, name: str, labels: List[Label], level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def patch_destination_labels(self, name: str, patch: PatchLabels, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... + def create_destination( + self, + dest: Destination, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def update_destination( + self, + dest: Destination, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def delete_destination( + self, + name: str, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def get_destination_labels( + self, + name: str, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> List[Label]: ... + def update_destination_labels( + self, + name: str, + labels: List[Label], + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def patch_destination_labels( + self, + name: str, + patch: PatchLabels, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... # V2 Runtime API - Destination consumption with automatic token retrieval - def get_destination(self, name: str, level: Optional[ConsumptionLevel] = None, options: Optional[ConsumptionOptions] = None, proxy_enabled: Optional[bool] = None, tenant: Optional[str] = None) -> Optional[Destination | TransparentProxyDestination]: ... + def get_destination( + self, + name: str, + level: Optional[ConsumptionLevel] = None, + options: Optional[ConsumptionOptions] = None, + proxy_enabled: Optional[bool] = None, + tenant: Optional[str] = None, + ) -> Optional[Destination | TransparentProxyDestination]: ... ``` ### Fragment Client @@ -150,15 +259,59 @@ The fragment client produced by `create_fragment_client()` exposes the following ```python class FragmentClient: def get_instance_fragment(self, name: str) -> Optional[Fragment]: ... - def get_subaccount_fragment(self, name: str, access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, tenant: Optional[str] = None) -> Optional[Fragment]: ... - def list_instance_fragments(self, tenant: Optional[str] = None, filter: Optional[ListOptions] = None) -> List[Fragment]: ... - def list_subaccount_fragments(self, access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, tenant: Optional[str] = None, filter: Optional[ListOptions] = None) -> List[Fragment]: ... - def create_fragment(self, fragment: Fragment, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def update_fragment(self, fragment: Fragment, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def delete_fragment(self, name: str, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def get_fragment_labels(self, name: str, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> List[Label]: ... - def update_fragment_labels(self, name: str, labels: List[Label], level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def patch_fragment_labels(self, name: str, patch: PatchLabels, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... + def get_subaccount_fragment( + self, + name: str, + access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, + tenant: Optional[str] = None, + ) -> Optional[Fragment]: ... + def list_instance_fragments( + self, tenant: Optional[str] = None, filter: Optional[ListOptions] = None + ) -> List[Fragment]: ... + def list_subaccount_fragments( + self, + access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, + tenant: Optional[str] = None, + filter: Optional[ListOptions] = None, + ) -> List[Fragment]: ... + def create_fragment( + self, + fragment: Fragment, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def update_fragment( + self, + fragment: Fragment, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def delete_fragment( + self, + name: str, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def get_fragment_labels( + self, + name: str, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> List[Label]: ... + def update_fragment_labels( + self, + name: str, + labels: List[Label], + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def patch_fragment_labels( + self, + name: str, + patch: PatchLabels, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... ``` ### Certificate Client @@ -168,15 +321,59 @@ The certificate client produced by `create_certificate_client()` exposes the fol ```python class CertificateClient: def get_instance_certificate(self, name: str) -> Optional[Certificate]: ... - def get_subaccount_certificate(self, name: str, access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, tenant: Optional[str] = None) -> Optional[Certificate]: ... - def list_instance_certificates(self, tenant: Optional[str] = None, filter: Optional[ListOptions] = None) -> PagedResult[Certificate]: ... - def list_subaccount_certificates(self, access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, tenant: Optional[str] = None, filter: Optional[ListOptions] = None) -> PagedResult[Certificate]: ... - def create_certificate(self, certificate: Certificate, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def update_certificate(self, certificate: Certificate, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def delete_certificate(self, name: str, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def get_certificate_labels(self, name: str, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> List[Label]: ... - def update_certificate_labels(self, name: str, labels: List[Label], level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... - def patch_certificate_labels(self, name: str, patch: PatchLabels, level: Optional[Level] = Level.SUB_ACCOUNT, tenant: Optional[str] = None) -> None: ... + def get_subaccount_certificate( + self, + name: str, + access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, + tenant: Optional[str] = None, + ) -> Optional[Certificate]: ... + def list_instance_certificates( + self, tenant: Optional[str] = None, filter: Optional[ListOptions] = None + ) -> PagedResult[Certificate]: ... + def list_subaccount_certificates( + self, + access_strategy: AccessStrategy = AccessStrategy.SUBSCRIBER_FIRST, + tenant: Optional[str] = None, + filter: Optional[ListOptions] = None, + ) -> PagedResult[Certificate]: ... + def create_certificate( + self, + certificate: Certificate, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def update_certificate( + self, + certificate: Certificate, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def delete_certificate( + self, + name: str, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def get_certificate_labels( + self, + name: str, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> List[Label]: ... + def update_certificate_labels( + self, + name: str, + labels: List[Label], + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... + def patch_certificate_labels( + self, + name: str, + patch: PatchLabels, + level: Optional[Level] = Level.SUB_ACCOUNT, + tenant: Optional[str] = None, + ) -> None: ... ``` ### Models @@ -310,7 +507,9 @@ from sap_cloud_sdk.destination import create_client, TransparentProxy client = create_client(instance="default") # Set custom proxy configuration -transparent_proxy = TransparentProxy(proxy_name="my-destination", namespace="my-namespace") +transparent_proxy = TransparentProxy( + proxy_name="my-destination", namespace="my-namespace" +) client.set_proxy(transparent_proxy) ``` @@ -350,15 +549,16 @@ dest = client.get_instance_destination("my-destination", proxy_enabled=True) # Example 3: Update proxy after creation client = create_client(instance="default") -transparent_proxy = TransparentProxy(proxy_name="my-destination", namespace="my-namespace") +transparent_proxy = TransparentProxy( + proxy_name="my-destination", namespace="my-namespace" +) client.set_proxy(transparent_proxy) dest = client.get_instance_destination("my-destination", proxy_enabled=True) # Example 4: Subaccount destination with proxy client = create_client(instance="default", use_default_proxy=True) dest = client.get_subaccount_destination( - name="my-destination", - access_strategy=AccessStrategy.PROVIDER_ONLY + name="my-destination", access_strategy=AccessStrategy.PROVIDER_ONLY ) # Uses proxy by default (client's use_default_proxy=True) @@ -391,7 +591,9 @@ dest = client.get_destination("my-api") # Example 9: Combine level with options options = ConsumptionOptions(fragment_name="production", tenant="tenant-1") -dest = client.get_destination("my-api", level=ConsumptionLevel.SUBACCOUNT, options=options) +dest = client.get_destination( + "my-api", level=ConsumptionLevel.SUBACCOUNT, options=options +) # Example 9b: Fragment with level hint options = ConsumptionOptions( @@ -453,9 +655,9 @@ When `proxy_enabled=True` (either as client default or per-request override), th client = create_client(instance="default", use_default_proxy=True) proxy_dest = client.get_instance_destination("my-destination") -print(proxy_dest.name) # "my-destination" -print(proxy_dest.url) # "http://connectivity-proxy.my-namespace" -print(proxy_dest.headers) # {"X-destination-name": "my-destination"} +print(proxy_dest.name) # "my-destination" +print(proxy_dest.url) # "http://connectivity-proxy.my-namespace" +print(proxy_dest.headers) # {"X-destination-name": "my-destination"} ``` ### Setting Custom Headers @@ -463,20 +665,14 @@ print(proxy_dest.headers) # {"X-destination-name": "my-destination"} The `TransparentProxyDestination` class provides a `set_header()` method to add or update headers required by the transparent proxy. Use the `TransparentProxyHeader` enum to ensure type-safe header names: ```python -from sap_cloud_sdk.destination import ( - create_client, - TransparentProxyHeader -) +from sap_cloud_sdk.destination import create_client, TransparentProxyHeader # Get a transparent proxy destination client = create_client(instance="default", use_default_proxy=True) proxy_dest = client.get_instance_destination("my-destination") # Set additional headers using the enum -proxy_dest.set_header( - TransparentProxyHeader.AUTHORIZATION, - "Bearer token123" -) +proxy_dest.set_header(TransparentProxyHeader.AUTHORIZATION, "Bearer token123") # Access the updated headers print(proxy_dest.headers) @@ -550,8 +746,13 @@ All three accept an optional `tenant` parameter (like the create/update/delete m ```python from sap_cloud_sdk.destination import ( - create_client, create_fragment_client, create_certificate_client, - Label, PatchLabels, ListOptions, Level + create_client, + create_fragment_client, + create_certificate_client, + Label, + PatchLabels, + ListOptions, + Level, ) client = create_client(instance="default") @@ -562,11 +763,15 @@ certificate_client = create_certificate_client(instance="default") labels = client.get_destination_labels("my-dest", level=Level.SUB_ACCOUNT) # Get labels in subscriber context -labels = client.get_destination_labels("my-dest", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") +labels = client.get_destination_labels( + "my-dest", level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) # Replace all labels (PUT) new_labels = [Label(key="env", values=["prod"]), Label(key="team", values=["platform"])] -client.update_destination_labels("my-dest", new_labels, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") +client.update_destination_labels( + "my-dest", new_labels, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) # Add labels incrementally (PATCH ADD — upserts existing keys) client.patch_destination_labels( @@ -584,7 +789,9 @@ client.patch_destination_labels( ) # Fragment and certificate label operations follow the same pattern -fragment_client.update_fragment_labels("my-fragment", new_labels, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain") +fragment_client.update_fragment_labels( + "my-fragment", new_labels, level=Level.SUB_ACCOUNT, tenant="tenant-subdomain" +) certificate_client.patch_certificate_labels( "my-cert.pem", PatchLabels(action="ADD", labels=[Label(key="env", values=["staging"])]), @@ -593,6 +800,7 @@ certificate_client.patch_certificate_labels( # Filter list results by label from sap_cloud_sdk.destination import ListOptions + filter_opts = ListOptions(filter_labels=[Label(key="env", values=["prod"])]) result = client.list_subaccount_destinations(filter=filter_opts) fragments = fragment_client.list_instance_fragments(filter=filter_opts) diff --git a/src/sap_cloud_sdk/dms/user-guide.md b/src/sap_cloud_sdk/dms/user-guide.md index 7d2969f4..9ee894c7 100644 --- a/src/sap_cloud_sdk/dms/user-guide.md +++ b/src/sap_cloud_sdk/dms/user-guide.md @@ -17,14 +17,29 @@ See further information about installation in the [main documentation](/README.m ```python from sap_cloud_sdk.dms import create_client from sap_cloud_sdk.dms.model import ( - InternalRepoRequest, UpdateRepoRequest, Repository, - CreateConfigRequest, UpdateConfigRequest, RepositoryConfig, ConfigName, - UserClaim, Ace, Acl, Folder, Document, CmisObject, ChildrenPage, + InternalRepoRequest, + UpdateRepoRequest, + Repository, + CreateConfigRequest, + UpdateConfigRequest, + RepositoryConfig, + ConfigName, + UserClaim, + Ace, + Acl, + Folder, + Document, + CmisObject, + ChildrenPage, ) from sap_cloud_sdk.dms.exceptions import ( - DMSError, DMSObjectNotFoundException, DMSPermissionDeniedException, - DMSInvalidArgumentException, DMSConflictException, - DMSConnectionError, DMSRuntimeException, + DMSError, + DMSObjectNotFoundException, + DMSPermissionDeniedException, + DMSInvalidArgumentException, + DMSConflictException, + DMSConnectionError, + DMSRuntimeException, ) ``` @@ -295,7 +310,7 @@ new_content = io.BytesIO(b"Updated content for v2") doc = client.check_in( repository_id="cmis-repo-id", - document_id=pwc.object_id, # the PWC objectId + document_id=pwc.object_id, # the PWC objectId major=True, file=new_content, file_name="hello.txt", @@ -436,7 +451,9 @@ page = client.get_children( order_by="cmis:creationDate ASC", ) -print(f"Items: {len(page.objects)}, hasMore: {page.has_more_items}, total: {page.num_items}") +print( + f"Items: {len(page.objects)}, hasMore: {page.has_more_items}, total: {page.num_items}" +) for obj in page.objects: kind = "Folder" if isinstance(obj, Folder) else "Document" diff --git a/src/sap_cloud_sdk/extensibility/user-guide.md b/src/sap_cloud_sdk/extensibility/user-guide.md index 25e2e012..4baa74fd 100644 --- a/src/sap_cloud_sdk/extensibility/user-guide.md +++ b/src/sap_cloud_sdk/extensibility/user-guide.md @@ -192,8 +192,8 @@ async def call_hook_agw( ```python @dataclass class N8nWorkflowConfig: - workflow_id: str # Workflow ID - method: HTTPMethod # HTTP method used by webhook execution + workflow_id: str # Workflow ID + method: HTTPMethod # HTTP method used by webhook execution ``` #### `HookConfig` @@ -203,9 +203,9 @@ Runtime invocation config required by `call_hook()`. ```python @dataclass class HookConfig: - endpoint: str # Full URL of the hook MCP endpoint - auth_token: Optional[str] # Bearer token for authentication - payload: Optional[dict] # Optional JSON payload + endpoint: str # Full URL of the hook MCP endpoint + auth_token: Optional[str] # Bearer token for authentication + payload: Optional[dict] # Optional JSON payload ``` ### `build_extension_capabilities()` @@ -236,12 +236,14 @@ Design-time declaration for A2A card serialization. ```python @dataclass class ExtensionCapability: - display_name: str # Human-readable name - description: str # Description of the capability - id: str = "default" # Capability identifier (only "default" currently supported) - tools: Tools = ... # Tool config (default: Tools(additions=ToolAdditions(enabled=True))) - instruction_supported: bool = True # Whether custom instructions are supported - supported_hooks: List[HookCapability] # List of supported hooks + display_name: str # Human-readable name + description: str # Description of the capability + id: str = "default" # Capability identifier (only "default" currently supported) + tools: Tools = ( + ... + ) # Tool config (default: Tools(additions=ToolAdditions(enabled=True))) + instruction_supported: bool = True # Whether custom instructions are supported + supported_hooks: List[HookCapability] # List of supported hooks ``` #### `Tools` @@ -271,7 +273,6 @@ Configuration for supported hook addition at an extension capability. ```python @dataclass class HookCapability: - id: str type: str display_name: str @@ -285,11 +286,11 @@ Runtime result returned by `ExtensibilityClient.get_extension_capability_impleme ```python @dataclass class ExtensionCapabilityImplementation: - capability_id: str # e.g. "default" - extension_names: List[str] = [] # Names of contributing extensions - mcp_servers: List[McpServer] = [] # MCP servers from the extension(s) - instruction: Optional[str] = None # Custom instruction text - hooks: List[Hook] = [] # Custom hooks registered as BEFORE or AFTER + capability_id: str # e.g. "default" + extension_names: List[str] = [] # Names of contributing extensions + mcp_servers: List[McpServer] = [] # MCP servers from the extension(s) + instruction: Optional[str] = None # Custom instruction text + hooks: List[Hook] = [] # Custom hooks registered as BEFORE or AFTER ``` #### `McpServer` @@ -299,9 +300,9 @@ An MCP server contributed by an extension. ```python @dataclass class McpServer: - ord_id: str # ORD ID, e.g. "sap.mcp:apiResource:serviceNow:v1" - global_tenant_id: str # Global tenant ID of the MCP server - tool_names: Optional[List[str]] = None # Approved tools (None = all approved) + ord_id: str # ORD ID, e.g. "sap.mcp:apiResource:serviceNow:v1" + global_tenant_id: str # Global tenant ID of the MCP server + tool_names: Optional[List[str]] = None # Approved tools (None = all approved) ``` - `global_tenant_id` is the global tenant ID of the MCP server. @@ -315,17 +316,19 @@ A workflow created as a hook to be executed. ```python @dataclass class Hook: - hook_id: str # Developer-facing hook key (not guaranteed unique) - id: str # Hook ID (UUID) - name: str # Human-readable name - type: HookType # Hook type (BEFORE, AFTER) - deployment_type: DeploymentType # Deployment type (N8N, SERVERLESS) - n8n_workflow_config: N8nWorkflowConfig # Workflow config (workflow ID + HTTP method) - timeout: int # Timeout in seconds - execution_mode: ExecutionMode # Execution mode (SYNC, ASYNC) - on_failure: OnFailure # Failure behavior (CONTINUE, BLOCK) - order: int # Execution order - can_short_circuit: bool # Whether hook can short-circuit execution + hook_id: str # Developer-facing hook key (not guaranteed unique) + id: str # Hook ID (UUID) + name: str # Human-readable name + type: HookType # Hook type (BEFORE, AFTER) + deployment_type: DeploymentType # Deployment type (N8N, SERVERLESS) + n8n_workflow_config: ( + N8nWorkflowConfig # Workflow config (workflow ID + HTTP method) + ) + timeout: int # Timeout in seconds + execution_mode: ExecutionMode # Execution mode (SYNC, ASYNC) + on_failure: OnFailure # Failure behavior (CONTINUE, BLOCK) + order: int # Execution order + can_short_circuit: bool # Whether hook can short-circuit execution ``` #### Hook Enums @@ -336,24 +339,24 @@ The `Hook` class uses several enums for type-safe field values: ```python class HookType(Enum): - BEFORE = "BEFORE" # Hook executed before an operation - AFTER = "AFTER" # Hook executed after an operation + BEFORE = "BEFORE" # Hook executed before an operation + AFTER = "AFTER" # Hook executed after an operation ``` **DeploymentType** ```python class DeploymentType(Enum): - N8N = "N8N" # Hook deployed on N8N platform - SERVERLESS = "SERVERLESS" # Hook deployed as Serverless function + N8N = "N8N" # Hook deployed on N8N platform + SERVERLESS = "SERVERLESS" # Hook deployed as Serverless function ``` **ExecutionMode** ```python class ExecutionMode(Enum): - SYNC = "SYNC" # Synchronous execution - waits for hook to complete - ASYNC = "ASYNC" # Asynchronous execution - does not wait for completion + SYNC = "SYNC" # Synchronous execution - waits for hook to complete + ASYNC = "ASYNC" # Asynchronous execution - does not wait for completion ``` **OnFailure** @@ -361,7 +364,7 @@ class ExecutionMode(Enum): ```python class OnFailure(Enum): CONTINUE = "CONTINUE" # Continue execution despite hook failure - BLOCK = "BLOCK" # Block execution when hook fails + BLOCK = "BLOCK" # Block execution when hook fails ``` **HTTPMethod** @@ -382,8 +385,8 @@ Optional configuration overrides for the extensibility service connection. ```python @dataclass class ExtensibilityConfig: - destination_name: Optional[str] = None # Optional destination name override - destination_instance: str = "default" # Destination service instance name + destination_name: Optional[str] = None # Optional destination name override + destination_instance: str = "default" # Destination service instance name ``` ### Constants @@ -521,7 +524,11 @@ if before_hooks: payload=Message( message_id="msg-hook-call-001", role=Role.user, - parts=[TextPart(text="Tool execution starting: create_ticket with priority=high")], + parts=[ + TextPart( + text="Tool execution starting: create_ticket with priority=high" + ) + ], ), ) @@ -802,7 +809,9 @@ Then use `create_client()` as usual -- it will automatically detect the file: ```python from sap_cloud_sdk.extensibility import create_client -client = create_client("sap.ai:agent:myAgent:v1") # Uses mocks/extensibility.json automatically +client = create_client( + "sap.ai:agent:myAgent:v1" +) # Uses mocks/extensibility.json automatically ext = client.get_extension_capability_implementation(tenant=tenant_id) ``` @@ -819,7 +828,9 @@ export CLOUD_SDK_LOCAL_EXTENSIBILITY_FILE=path/to/my/extensions.json ```python from sap_cloud_sdk.extensibility import create_client -client = create_client("sap.ai:agent:myAgent:v1") # Uses the file from the environment variable +client = create_client( + "sap.ai:agent:myAgent:v1" +) # Uses the file from the environment variable ext = client.get_extension_capability_implementation(tenant=tenant_id) ``` diff --git a/src/sap_cloud_sdk/ias/user-guide.md b/src/sap_cloud_sdk/ias/user-guide.md index 4f6c9c9f..f6957972 100644 --- a/src/sap_cloud_sdk/ias/user-guide.md +++ b/src/sap_cloud_sdk/ias/user-guide.md @@ -19,12 +19,14 @@ Use `parse_token` to decode an IAS JWT token into a typed dataclass. All standar ```python from sap_cloud_sdk.ias import parse_token -claims = parse_token(request.headers["Authorization"]) # accepts "Bearer " or raw token - -print(claims.app_tid) # tenant ID (multitenant scenarios) -print(claims.scim_id) # SCIM-based user ID in SAP Cloud Identity Services -print(claims.sub) # OIDC subject identifier -print(claims.email) # user email (when email scope was requested) +claims = parse_token( + request.headers["Authorization"] +) # accepts "Bearer " or raw token + +print(claims.app_tid) # tenant ID (multitenant scenarios) +print(claims.scim_id) # SCIM-based user ID in SAP Cloud Identity Services +print(claims.sub) # OIDC subject identifier +print(claims.email) # user email (when email scope was requested) ``` ### Claims Reference diff --git a/src/sap_cloud_sdk/objectstore/user-guide.md b/src/sap_cloud_sdk/objectstore/user-guide.md index 5864e458..58ae48ed 100644 --- a/src/sap_cloud_sdk/objectstore/user-guide.md +++ b/src/sap_cloud_sdk/objectstore/user-guide.md @@ -44,7 +44,7 @@ from sap_cloud_sdk.objectstore import create_client # Custom configuration with SSL disabled client = create_client( "my-instance", - disable_ssl=True # Disable SSL (default is False) + disable_ssl=True, # Disable SSL (default is False) ) ``` @@ -61,11 +61,7 @@ client = create_client( ```python # Upload binary data directly data = b"Hello, World!" -client.put_object_from_bytes( - name="hello.txt", - data=data, - content_type="text/plain" -) +client.put_object_from_bytes(name="hello.txt", data=data, content_type="text/plain") ``` ### From File @@ -75,7 +71,7 @@ client.put_object_from_bytes( client.put_object_from_file( name="document.pdf", file_path="/path/to/local/document.pdf", - content_type="application/pdf" + content_type="application/pdf", ) ``` @@ -90,20 +86,18 @@ client.put_object( name="stream.txt", stream=stream, size=len(b"Streamed content"), - content_type="text/plain" + content_type="text/plain", ) # Or with a real file object with open("/path/to/file.txt", "rb") as f: # Get file size import os + size = os.path.getsize("/path/to/file.txt") client.put_object( - name="uploaded.txt", - stream=f, - size=size, - content_type="text/plain" + name="uploaded.txt", stream=f, size=size, content_type="text/plain" ) ``` @@ -119,7 +113,7 @@ response = client.get_object("hello.txt") # Read the content content = response.read() # Returns bytes -text_content = content.decode('utf-8') # Convert to string if needed +text_content = content.decode("utf-8") # Convert to string if needed # Don't forget to close the response response.close() @@ -197,7 +191,7 @@ from sap_cloud_sdk.objectstore import ( ObjectNotFoundError, ObjectOperationError, ClientCreationError, - ListObjectsError + ListObjectsError, ) try: diff --git a/src/sap_cloud_sdk/print/user-guide.md b/src/sap_cloud_sdk/print/user-guide.md index 59b62855..b4913d30 100644 --- a/src/sap_cloud_sdk/print/user-guide.md +++ b/src/sap_cloud_sdk/print/user-guide.md @@ -16,10 +16,17 @@ See further information about installation in the [main documentation](/README.m ```python from sap_cloud_sdk.print import create_client from sap_cloud_sdk.print import ( - PrintQueue, PrintProfile, PrintContent, PrintTask, PrintTaskMetadata, + PrintQueue, + PrintProfile, + PrintContent, + PrintTask, + PrintTaskMetadata, ) from sap_cloud_sdk.print.exceptions import ( - ClientCreationError, ConfigError, PrintOperationError, HttpError, + ClientCreationError, + ConfigError, + PrintOperationError, + HttpError, ) ``` diff --git a/uv.lock b/uv.lock index c04d9d4b..3c3ea997 100644 --- a/uv.lock +++ b/uv.lock @@ -161,9 +161,9 @@ name = "aiologic" version = "0.16.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "sniffio" }, - { name = "typing-extensions" }, - { name = "wrapt" }, + { name = "sniffio", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, + { name = "wrapt", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/13/50b91a3ea6b030d280d2654be97c48b6ed81753a50286ee43c646ba36d3c/aiologic-0.16.0.tar.gz", hash = "sha256:c267ccbd3ff417ec93e78d28d4d577ccca115d5797cdbd16785a551d9658858f", size = 225952, upload-time = "2025-11-27T23:48:41.195Z" } wheels = [ @@ -615,8 +615,8 @@ name = "culsans" version = "0.11.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "aiologic" }, - { name = "typing-extensions" }, + { name = "aiologic", marker = "python_full_version < '3.13'" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/e3/49afa1bc180e0d28008ec6bcdf82a4072d1c7a41032b5b759b60814ca4b0/culsans-0.11.0.tar.gz", hash = "sha256:0b43d0d05dce6106293d114c86e3fb4bfc63088cfe8ff08ed3fe36891447fe33", size = 107546, upload-time = "2025-12-31T23:15:38.196Z" } wheels = [ @@ -665,9 +665,9 @@ resolution-markers = [ "python_full_version < '3.12'", ] dependencies = [ - { name = "asgiref" }, - { name = "sqlparse" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, + { name = "asgiref", marker = "python_full_version < '3.12'" }, + { name = "sqlparse", marker = "python_full_version < '3.12'" }, + { name = "tzdata", marker = "python_full_version < '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a9/26/889449d521ae508b26de715954faecd8bcf3f740affb81b2d146a83b42a5/django-5.2.16.tar.gz", hash = "sha256:59ea02020c3136fce14bef0bbece21a10a4febef5eed1c51c22ae468efa22200", size = 10890894, upload-time = "2026-07-07T13:52:17.005Z" } wheels = [ @@ -685,9 +685,9 @@ resolution-markers = [ "python_full_version == '3.12.*'", ] dependencies = [ - { name = "asgiref" }, - { name = "sqlparse" }, - { name = "tzdata", marker = "sys_platform == 'win32'" }, + { name = "asgiref", marker = "python_full_version >= '3.12'" }, + { name = "sqlparse", marker = "python_full_version >= '3.12'" }, + { name = "tzdata", marker = "python_full_version >= '3.12' and sys_platform == 'win32'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/89/55/664f24ff81c9ea19cb7dfc851afeae1f3c2390c7aee01d4ded68b5c1580d/django-6.0.7.tar.gz", hash = "sha256:2998503fc083124fb58037084bfa00de323c7c743f05f1b4284e77bff0ab8890", size = 10921299, upload-time = "2026-07-07T13:51:26.485Z" } wheels = [ @@ -3900,27 +3900,27 @@ wheels = [ [[package]] name = "ruff" -version = "0.15.5" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/77/9b/840e0039e65fcf12758adf684d2289024d6140cde9268cc59887dc55189c/ruff-0.15.5.tar.gz", hash = "sha256:7c3601d3b6d76dce18c5c824fc8d06f4eef33d6df0c21ec7799510cde0f159a2", size = 4574214, upload-time = "2026-03-05T20:06:34.946Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/20/5369c3ce21588c708bcbe517a8fbe1a8dfdb5dfd5137e14790b1da71612c/ruff-0.15.5-py3-none-linux_armv6l.whl", hash = "sha256:4ae44c42281f42e3b06b988e442d344a5b9b72450ff3c892e30d11b29a96a57c", size = 10478185, upload-time = "2026-03-05T20:06:29.093Z" }, - { url = "https://files.pythonhosted.org/packages/44/ed/e81dd668547da281e5dce710cf0bc60193f8d3d43833e8241d006720e42b/ruff-0.15.5-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:6edd3792d408ebcf61adabc01822da687579a1a023f297618ac27a5b51ef0080", size = 10859201, upload-time = "2026-03-05T20:06:32.632Z" }, - { url = "https://files.pythonhosted.org/packages/c4/8f/533075f00aaf19b07c5cd6aa6e5d89424b06b3b3f4583bfa9c640a079059/ruff-0.15.5-py3-none-macosx_11_0_arm64.whl", hash = "sha256:89f463f7c8205a9f8dea9d658d59eff49db05f88f89cc3047fb1a02d9f344010", size = 10184752, upload-time = "2026-03-05T20:06:40.312Z" }, - { url = "https://files.pythonhosted.org/packages/66/0e/ba49e2c3fa0395b3152bad634c7432f7edfc509c133b8f4529053ff024fb/ruff-0.15.5-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ba786a8295c6574c1116704cf0b9e6563de3432ac888d8f83685654fe528fd65", size = 10534857, upload-time = "2026-03-05T20:06:19.581Z" }, - { url = "https://files.pythonhosted.org/packages/59/71/39234440f27a226475a0659561adb0d784b4d247dfe7f43ffc12dd02e288/ruff-0.15.5-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fd4b801e57955fe9f02b31d20375ab3a5c4415f2e5105b79fb94cf2642c91440", size = 10309120, upload-time = "2026-03-05T20:06:00.435Z" }, - { url = "https://files.pythonhosted.org/packages/f5/87/4140aa86a93df032156982b726f4952aaec4a883bb98cb6ef73c347da253/ruff-0.15.5-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:391f7c73388f3d8c11b794dbbc2959a5b5afe66642c142a6effa90b45f6f5204", size = 11047428, upload-time = "2026-03-05T20:05:51.867Z" }, - { url = "https://files.pythonhosted.org/packages/5a/f7/4953e7e3287676f78fbe85e3a0ca414c5ca81237b7575bdadc00229ac240/ruff-0.15.5-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8dc18f30302e379fe1e998548b0f5e9f4dff907f52f73ad6da419ea9c19d66c8", size = 11914251, upload-time = "2026-03-05T20:06:22.887Z" }, - { url = "https://files.pythonhosted.org/packages/77/46/0f7c865c10cf896ccf5a939c3e84e1cfaeed608ff5249584799a74d33835/ruff-0.15.5-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1cc6e7f90087e2d27f98dc34ed1b3ab7c8f0d273cc5431415454e22c0bd2a681", size = 11333801, upload-time = "2026-03-05T20:05:57.168Z" }, - { url = "https://files.pythonhosted.org/packages/d3/01/a10fe54b653061585e655f5286c2662ebddb68831ed3eaebfb0eb08c0a16/ruff-0.15.5-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c1cb7169f53c1ddb06e71a9aebd7e98fc0fea936b39afb36d8e86d36ecc2636a", size = 11206821, upload-time = "2026-03-05T20:06:03.441Z" }, - { url = "https://files.pythonhosted.org/packages/7a/0d/2132ceaf20c5e8699aa83da2706ecb5c5dcdf78b453f77edca7fb70f8a93/ruff-0.15.5-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:9b037924500a31ee17389b5c8c4d88874cc6ea8e42f12e9c61a3d754ff72f1ca", size = 11133326, upload-time = "2026-03-05T20:06:25.655Z" }, - { url = "https://files.pythonhosted.org/packages/72/cb/2e5259a7eb2a0f87c08c0fe5bf5825a1e4b90883a52685524596bfc93072/ruff-0.15.5-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:65bb414e5b4eadd95a8c1e4804f6772bbe8995889f203a01f77ddf2d790929dd", size = 10510820, upload-time = "2026-03-05T20:06:37.79Z" }, - { url = "https://files.pythonhosted.org/packages/ff/20/b67ce78f9e6c59ffbdb5b4503d0090e749b5f2d31b599b554698a80d861c/ruff-0.15.5-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d20aa469ae3b57033519c559e9bc9cd9e782842e39be05b50e852c7c981fa01d", size = 10302395, upload-time = "2026-03-05T20:05:54.504Z" }, - { url = "https://files.pythonhosted.org/packages/5f/e5/719f1acccd31b720d477751558ed74e9c88134adcc377e5e886af89d3072/ruff-0.15.5-py3-none-musllinux_1_2_i686.whl", hash = "sha256:15388dd28c9161cdb8eda68993533acc870aa4e646a0a277aa166de9ad5a8752", size = 10754069, upload-time = "2026-03-05T20:06:06.422Z" }, - { url = "https://files.pythonhosted.org/packages/c3/9c/d1db14469e32d98f3ca27079dbd30b7b44dbb5317d06ab36718dee3baf03/ruff-0.15.5-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b30da330cbd03bed0c21420b6b953158f60c74c54c5f4c1dabbdf3a57bf355d2", size = 11304315, upload-time = "2026-03-05T20:06:10.867Z" }, - { url = "https://files.pythonhosted.org/packages/28/3a/950367aee7c69027f4f422059227b290ed780366b6aecee5de5039d50fa8/ruff-0.15.5-py3-none-win32.whl", hash = "sha256:732e5ee1f98ba5b3679029989a06ca39a950cced52143a0ea82a2102cb592b74", size = 10551676, upload-time = "2026-03-05T20:06:13.705Z" }, - { url = "https://files.pythonhosted.org/packages/b8/00/bf077a505b4e649bdd3c47ff8ec967735ce2544c8e4a43aba42ee9bf935d/ruff-0.15.5-py3-none-win_amd64.whl", hash = "sha256:821d41c5fa9e19117616c35eaa3f4b75046ec76c65e7ae20a333e9a8696bc7fe", size = 11678972, upload-time = "2026-03-05T20:06:45.379Z" }, - { url = "https://files.pythonhosted.org/packages/fe/4e/cd76eca6db6115604b7626668e891c9dd03330384082e33662fb0f113614/ruff-0.15.5-py3-none-win_arm64.whl", hash = "sha256:b498d1c60d2fe5c10c45ec3f698901065772730b411f164ae270bb6bfcc4740b", size = 10965572, upload-time = "2026-03-05T20:06:16.984Z" }, +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4d/94/1e5e4967626faf12fa56999cd6222dff6992ceb086ad7945756baf70c7a7/ruff-0.16.0.tar.gz", hash = "sha256:e460aafd5495ec89efaa6ced2e4a9a581116451e1c88b9d37ef497e0f8e93982", size = 4790557, upload-time = "2026-07-23T19:11:30.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/81/1c8818fee7ce1a04cd7d1b3172e0a8f8e4f1dc4feb7fc390e16daa8af323/ruff-0.16.0-py3-none-linux_armv6l.whl", hash = "sha256:e5115729eb08c585e5121978ba5d5b60caeae394ce21b9fb5e6cd33a1c6c9b1e", size = 10754633, upload-time = "2026-07-23T19:10:46.415Z" }, + { url = "https://files.pythonhosted.org/packages/23/df/beaf59c09d68db84304d555f188b276a77132a5d5b0b67a5c762aa143628/ruff-0.16.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:3c954b1d580bfa035b41654f7858cc7e71d5fc3ac5b723dd62bd9133830ed522", size = 10969164, upload-time = "2026-07-23T19:10:50.271Z" }, + { url = "https://files.pythonhosted.org/packages/42/ce/741cd197496a1abbf51352710fd15ed995d2a2be87189c1da26a450d6e83/ruff-0.16.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e01c21d10eb1b29f47b7454e1f4056db9a3f0260c646aa88457c610291db9f81", size = 10488846, upload-time = "2026-07-23T19:10:52.639Z" }, + { url = "https://files.pythonhosted.org/packages/52/2a/a2db8e88cade358f5cdcb05674a917751074109315d014eb6352d9a893f7/ruff-0.16.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e364e5ed22ed8dc05082fd78e35308618260907ac2d3c1d637b2e682415b6c9", size = 10889729, upload-time = "2026-07-23T19:10:54.89Z" }, + { url = "https://files.pythonhosted.org/packages/42/65/62a771694ebd63029dc953e27dbad40e1588bd4860ff9fe881018fddaa49/ruff-0.16.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d327b8fc113a1d4421a04f3839d3752057c8dd1ee320223a6f3f52d04ada462a", size = 10568275, upload-time = "2026-07-23T19:10:56.993Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e2/ced249fe8af5f086c5c58cc21cc3356d50f32f7401c5df87050c999620a7/ruff-0.16.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9b50c55e263103586b3dcf5f73d479eb8cb5fdb6098fec59a62891dab653717", size = 11385112, upload-time = "2026-07-23T19:10:59.615Z" }, + { url = "https://files.pythonhosted.org/packages/87/0b/05154977a8fd69eeb6c103271f55403bfd8711f5c0f8ed07489d95a504e7/ruff-0.16.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0ff4a79ce3ec0172f3241943835de1c4cb4e2dcd07f0f8c2d02603dbbbee4b17", size = 12207008, upload-time = "2026-07-23T19:11:02.154Z" }, + { url = "https://files.pythonhosted.org/packages/fb/29/98225831a3a1eab0e02f4acc6ca6559a98611dcc68b6965ff4b7234627c1/ruff-0.16.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e95c448fca1fb2a18372a9440926c5a6ee789639bb975c72e7ae6d0b04218ab4", size = 11650842, upload-time = "2026-07-23T19:11:04.557Z" }, + { url = "https://files.pythonhosted.org/packages/91/66/6bd3cf90500653d55dc0ffc8507aa8300bd49d0214b2e8cb4d3fef2943ba/ruff-0.16.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f11a8d11010301d0a398a2fdef67691feca7294da6aef55e2150e8fa2cd520b", size = 11400718, upload-time = "2026-07-23T19:11:09.233Z" }, + { url = "https://files.pythonhosted.org/packages/8e/a2/a54eb4eae05d66364050a5d3b8a9c5ef88196531b3cbe7109d873f87f819/ruff-0.16.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:48044c678e9cb8698246c99b14aaccfa6601dea7379eb48a6f8f73f7a6d86cd0", size = 11426177, upload-time = "2026-07-23T19:11:11.994Z" }, + { url = "https://files.pythonhosted.org/packages/1a/be/16e3eea4b2a478a496919f5e36f17c4559e54620bd3bbac5d6affa068006/ruff-0.16.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:7aa0959bad8eb8bef50340154fc9b58678dae31fa4293afa38b44b6e552c0213", size = 10856126, upload-time = "2026-07-23T19:11:14.221Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/252eb8b868a16eec7257c14f504f77537e734b2d69c762e639e588e304a3/ruff-0.16.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:28ea2b7df8ebf7f9da6b7d47b230ab48f387c0a29be3b474c4d0740e197bb9af", size = 10571208, upload-time = "2026-07-23T19:11:16.378Z" }, + { url = "https://files.pythonhosted.org/packages/21/09/817a482f542f7570cbb4554b26e896610c7114f539b1d9e2d2145bf6bef6/ruff-0.16.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:33a3dfac8c35f81498dea9181bccc2f4c4bc8f1521a1dd9406e77643e0f0fb09", size = 11063329, upload-time = "2026-07-23T19:11:19.173Z" }, + { url = "https://files.pythonhosted.org/packages/2e/23/9403c180ca1cb9b1f7335f5c3e5305c09d49ea5b345196682a36028bde4a/ruff-0.16.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:a5237a0bda500d30d81b8e07a6973a5cbc772864cbf746ae2f4e8a2e01c9f4ed", size = 11489751, upload-time = "2026-07-23T19:11:21.74Z" }, + { url = "https://files.pythonhosted.org/packages/b2/1d/1b2ef7bcde851c78d7f17f1cca13fd6dc695fc4b3d6197941e72cae5b132/ruff-0.16.0-py3-none-win32.whl", hash = "sha256:7fab76fa065c873f41ff744347c6e77bcc3dfec4bcc754dc26b63d23c0f7f5fb", size = 10785885, upload-time = "2026-07-23T19:11:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/b2/a3/d5e4ef7a56be3f928ffb90b94c25ba7d3cb9c7fe0736aeaaedf361770712/ruff-0.16.0-py3-none-win_amd64.whl", hash = "sha256:429c117f022bf481fabd9d551e7a3952b24c65e6ef44337ea09d90bebef14472", size = 11923141, upload-time = "2026-07-23T19:11:26.409Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9a/8415f2657cbe200f41a4531ccededf135505a92d4a012229121f885b26f9/ruff-0.16.0-py3-none-win_arm64.whl", hash = "sha256:14296fedcd2705c77ab8235439278bbb38f285cf7da5528b00b3e330c3d4872d", size = 11273407, upload-time = "2026-07-23T19:11:28.705Z" }, ] [[package]] @@ -4073,7 +4073,7 @@ dev = [ { name = "pytest-bdd", specifier = ">=7.2.0" }, { name = "pytest-cov", specifier = ">=7.0.0" }, { name = "python-dotenv", specifier = ">=1.0.0" }, - { name = "ruff", specifier = "==0.15.5" }, + { name = "ruff", specifier = "==0.16.0" }, { name = "sqlalchemy", specifier = ">=2.0.0" }, { name = "starlette", specifier = ">=0.40.0" }, { name = "ty", specifier = "==0.0.64" },