Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "sap-cloud-sdk"
version = "0.36.1"
version = "0.36.2"
description = "SAP Cloud SDK for Python"
readme = "README.md"
license = "Apache-2.0"
Expand All @@ -15,17 +15,17 @@ dependencies = [
"requests-oauthlib~=2.0.0",
"pydantic~=2.12.3",
"hatchling~=1.27.0",
"opentelemetry-exporter-otlp-proto-grpc~=1.42.1",
"opentelemetry-exporter-otlp-proto-http~=1.42.1",
"opentelemetry-exporter-otlp-proto-grpc~=1.44.0",
"opentelemetry-exporter-otlp-proto-http~=1.44.0",
"traceloop-sdk~=0.61.0",
"opentelemetry-instrumentation-langchain>=0.61.0,<1",
"httpx>=0.27.0,<1",
"PyJWT>=2.13.0,<3",
"protobuf>=5.0.0,<8",
"protovalidate>=0.13.0,<1",
"protobuf>=6.33.5,<7",
"protovalidate>=1.0.0,<2",
"grpcio>=1.60.0,<2",
"opentelemetry-api>=1.42.1,<2",
"opentelemetry-sdk>=1.42.1,<2",
"opentelemetry-api~=1.44.0",
"opentelemetry-sdk~=1.44.0",
"mcp>=1.1.0,<2",
]

Expand All @@ -50,7 +50,7 @@ dev = [
"pytest-bdd>=8.1.0,<9",
"python-dotenv>=1.0.0,<2",
"ty>=0.0.21,<1",
"cryptography>=46.0.3,<47",
"cryptography>=48.0.1,<51",
"ruff>=0.8.0,<1",
"starlette>=0.40.0,<1",
"anyio>=4.5,<5",
Expand Down
3 changes: 2 additions & 1 deletion src/sap_cloud_sdk/adms/_async_http.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
from __future__ import annotations

import asyncio
import inspect
from typing import Any, Callable, Dict, Optional

import httpx
Expand Down Expand Up @@ -216,7 +217,7 @@ async def _bearer_token(self) -> Optional[str]:
"""Resolve the bearer token, handling both sync and async callables."""
if self._get_token is None:
return None
if asyncio.iscoroutinefunction(self._get_token):
if inspect.iscoroutinefunction(self._get_token):
return await self._get_token()
return await asyncio.to_thread(self._get_token)

Expand Down
6 changes: 6 additions & 0 deletions src/sap_cloud_sdk/core/auditlog_ng/buf.lock
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# Generated by buf. DO NOT EDIT.
version: v2
deps:
- name: buf.build/bufbuild/protovalidate
commit: 511051f7f4374c3ca873b53ae68a9288
digest: b5:a4a2d4d808a25984cced60769c822c5d496ef0b740f56ac0c9e6b97aaa25b86a9332a00ffd74e0cd202be29e91bd3edfb0bf2ba4dacfe48ff2d8217f9986e3c8
Original file line number Diff line number Diff line change
Expand Up @@ -182,10 +182,10 @@ def _post_file_request(
try:
if request.file_path is not None:
file_handle = open(request.file_path, "rb")
file_value = file_handle
else:
file_value = request.file_content

file_value: BinaryIO | bytes = (
file_handle if file_handle is not None else request.file_content or b""
)
files = {
"file": (
request.resolved_file_name(),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,11 @@ def _normalize_attributes(self, span: ReadableSpan) -> None:
if not hasattr(span, "_attributes") or span._attributes is None:
return

# BoundedAttributes (the default OTEL type) is immutable; copy to a plain dict
# so mutations below don't silently fail with TypeError.
if not isinstance(span._attributes, dict):
span._attributes = dict(span._attributes)

attrs = cast(MutableMapping[str, Any], span._attributes)

# Only consider spans that have traceloop.* or llm.* or gen_ai.prompt.* or gen_ai.completion.* attributes
Expand Down
2 changes: 1 addition & 1 deletion tests/adms/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ def adms_config() -> AdmsConfig:
try:
return load_from_env_or_mount("default")
except ConfigError as exc:
pytest.skip(f"ADMS integration tests skipped — missing config: {exc}")
pytest.skip(f"ADMS integration tests skipped — missing config: {exc}") # ty: ignore[too-many-positional-arguments]


# ---------------------------------------------------------------------------
Expand Down
2 changes: 1 addition & 1 deletion tests/adms/integration/test_e2e_async_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,7 @@ def async_scan_state_pending_or_clean(context: AsyncScenarioContext) -> None:
@then("the async download should be blocked if not CLEAN")
def async_download_blocked_if_not_clean(context: AsyncScenarioContext) -> None:
if context.download_blocked is False:
pytest.skip("Document already CLEAN — scan gate test not applicable")
pytest.skip("Document already CLEAN — scan gate test not applicable") # ty: ignore[too-many-positional-arguments]
assert context.download_blocked is True


Expand Down
2 changes: 1 addition & 1 deletion tests/adms/integration/test_e2e_document_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -288,7 +288,7 @@ def scan_state_pending_or_clean(context: ScenarioContext) -> None:
@then("the download should be blocked if not CLEAN")
def download_blocked_if_not_clean(context: ScenarioContext) -> None:
if context.download_blocked is False:
pytest.skip("Document already CLEAN — scan gate test not applicable")
pytest.skip("Document already CLEAN — scan gate test not applicable") # ty: ignore[too-many-positional-arguments]
assert context.download_blocked is True


Expand Down
8 changes: 4 additions & 4 deletions tests/agent_memory/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ def agent_memory_client() -> AgentMemoryClient:
try:
return create_client(access_strategy=AccessStrategy.PROVIDER)
except AgentMemoryConfigError as e:
pytest.skip(f"Agent Memory credentials not configured — skipping integration tests: {e}")
pytest.skip(f"Agent Memory credentials not configured — skipping integration tests: {e}") # ty: ignore[too-many-positional-arguments]
except Exception as e:
pytest.fail(f"Failed to create Agent Memory client for integration tests: {e}")
pytest.fail(f"Failed to create Agent Memory client for integration tests: {e}") # ty: ignore[invalid-argument-type]


@pytest.fixture(scope="session")
Expand All @@ -71,15 +71,15 @@ def subscriber_tenant() -> str:
tenant = os.environ.get("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_SUBSCRIBER_TENANT", "")
if not tenant:
pytest.skip(
"CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_SUBSCRIBER_TENANT not set — "
"CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_SUBSCRIBER_TENANT not set — " # ty: ignore[too-many-positional-arguments]
"skipping subscriber tenant tests"
)

try:
_load_config_for_instance(tenant)
except AgentMemoryConfigError:
pytest.skip(
f"Subscriber binding for tenant '{tenant}' not configured — "
f"Subscriber binding for tenant '{tenant}' not configured — " # ty: ignore[too-many-positional-arguments]
f"skipping subscriber tenant tests"
)

Expand Down
8 changes: 4 additions & 4 deletions tests/agentgateway/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ def agw_client() -> AgentGatewayClient:

tenant_subdomain = os.environ.get("CLOUD_SDK_CFG_AGW_DEFAULT_TENANT_SUBDOMAIN")
if not tenant_subdomain:
pytest.skip("CLOUD_SDK_CFG_AGW_DEFAULT_TENANT_SUBDOMAIN is not set — skipping AGW integration tests")
pytest.skip("CLOUD_SDK_CFG_AGW_DEFAULT_TENANT_SUBDOMAIN is not set — skipping AGW integration tests") # ty: ignore[too-many-positional-arguments]

landscape = os.environ.get("CLOUD_SDK_CFG_AGW_DEFAULT_LANDSCAPE")
if landscape:
Expand All @@ -33,9 +33,9 @@ def agw_client() -> AgentGatewayClient:
try:
return create_client(tenant_subdomain=tenant_subdomain)
except MCPServerNotFoundError as e:
pytest.skip(f"AGW not subscribed for this tenant — skipping AGW integration tests: {e}")
pytest.skip(f"AGW not subscribed for this tenant — skipping AGW integration tests: {e}") # ty: ignore[too-many-positional-arguments]
except Exception as e:
pytest.fail(f"Failed to create Agent Gateway client for integration tests: {e}")
pytest.fail(f"Failed to create Agent Gateway client for integration tests: {e}") # ty: ignore[invalid-argument-type]


# Configure pytest markers for integration tests
Expand All @@ -59,4 +59,4 @@ def pytest_runtest_call(item):
try:
item.runtest()
except MCPServerNotFoundError as e:
pytest.skip(f"AGW not subscribed for this tenant — skipping: {e}")
pytest.skip(f"AGW not subscribed for this tenant — skipping: {e}") # ty: ignore[too-many-positional-arguments]
6 changes: 3 additions & 3 deletions tests/agentgateway/integration/test_agw_bdd.py
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,7 @@ def have_valid_user_token(context: ScenarioContext):
"""Load user token from environment variable."""
token = os.environ.get("CLOUD_SDK_CFG_AGW_DEFAULT_USER_TOKEN", "")
if not token:
pytest.skip("CLOUD_SDK_CFG_AGW_DEFAULT_USER_TOKEN is not set — skipping user auth scenario")
pytest.skip("CLOUD_SDK_CFG_AGW_DEFAULT_USER_TOKEN is not set — skipping user auth scenario") # ty: ignore[too-many-positional-arguments]
context.user_token = token


Expand All @@ -86,7 +86,7 @@ def have_sample_mcp_tool_name(context: ScenarioContext):
"""Load sample MCP tool name from environment variable."""
tool_name = os.environ.get("CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL", "")
if not tool_name:
pytest.skip("CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL is not set — skipping tool scenario")
pytest.skip("CLOUD_SDK_CFG_AGW_DEFAULT_SAMPLE_MCP_TOOL is not set — skipping tool scenario") # ty: ignore[too-many-positional-arguments]
context.sample_mcp_tool_name = tool_name


Expand Down Expand Up @@ -141,7 +141,7 @@ def call_call_mcp_tool_sample(context: ScenarioContext, agw_client: AgentGateway
assert context.sample_mcp_tool_name is not None
tool = next((t for t in context.tools if t.name == context.sample_mcp_tool_name), None)
if tool is None:
pytest.fail(f"Tool '{context.sample_mcp_tool_name}' not found in list_mcp_tools result")
pytest.fail(f"Tool '{context.sample_mcp_tool_name}' not found in list_mcp_tools result") # ty: ignore[invalid-argument-type]
context.tool_result = run(
agw_client.call_mcp_tool(tool, user_token=context.user_token)
)
Expand Down
2 changes: 1 addition & 1 deletion tests/aicore/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def aicore_configured():
_load_env()
missing = [k for k in _REQUIRED_VARS if not os.environ.get(k)]
if missing:
pytest.skip(f"Missing env vars for filtering integration tests: {missing}")
pytest.skip(f"Missing env vars for filtering integration tests: {missing}") # ty: ignore[too-many-positional-arguments]
set_aicore_config()
yield
disable_filtering()
Expand Down
2 changes: 1 addition & 1 deletion tests/aicore/integration/test_filtering_bdd.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,7 +151,7 @@ def send_prompt(ctx: ScenarioContext, model: str, prompt: str) -> None:
"""
if not prompt:
pytest.skip(
"Self-harm test prompt is empty — set the "
"Self-harm test prompt is empty — set the " # ty: ignore[too-many-positional-arguments]
"AICORE_FILTER_TEST_SELF_HARM_PROMPT environment variable "
"(GitHub secret in CI) to a prompt that triggers Azure Content "
"Safety self-harm filtering. Kept out of source so harmful "
Expand Down
2 changes: 1 addition & 1 deletion tests/core/integration/auditlog/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ def auditlog_client():
client = create_client()
return client
except Exception as e:
pytest.fail(f"Failed to create AuditLog client for cloud integration tests: {e}")
pytest.fail(f"Failed to create AuditLog client for cloud integration tests: {e}") # ty: ignore[invalid-argument-type]


@pytest.fixture
Expand Down
2 changes: 1 addition & 1 deletion tests/core/integration/auditlog/test_auditlog_bdd.py
Original file line number Diff line number Diff line change
Expand Up @@ -744,7 +744,7 @@ def some_events_succeed_some_fail(context):
return

if len(context.concurrent_results) == 0 and len(context.individual_attempts) == 0:
pytest.fail("Expected mixed success and failures, but no failures were simulated. Check intermittent connectivity setup.")
pytest.fail("Expected mixed success and failures, but no failures were simulated. Check intermittent connectivity setup.") # ty: ignore[invalid-argument-type]

successful_count = sum(1 for r in context.concurrent_results if r.get("success", False))
failed_count = len(context.concurrent_results) - successful_count
Expand Down
2 changes: 1 addition & 1 deletion tests/core/integration/telemetry/_agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def build_langgraph_agent():
from langgraph.graph import END, StateGraph
from langgraph.graph.message import add_messages
except ImportError:
pytest.skip("langchain-litellm or langgraph not installed")
pytest.skip("langchain-litellm or langgraph not installed") # ty: ignore[too-many-positional-arguments]

@dataclass
class State:
Expand Down
2 changes: 1 addition & 1 deletion tests/core/integration/telemetry/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,5 @@ def aicore_configured():
skipped automatically.
"""
if not os.environ.get("AICORE_BASE_URL"):
pytest.skip("AICORE_BASE_URL not set — skipping AI Core integration tests")
pytest.skip("AICORE_BASE_URL not set — skipping AI Core integration tests") # ty: ignore[too-many-positional-arguments]
set_aicore_config()
4 changes: 3 additions & 1 deletion tests/core/unit/data_anonymization/test_http_transport.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,7 @@ def test_resolve_cert_from_destination(
transport._tmp_key_file = None

cert_path = transport._resolve_cert()
assert isinstance(cert_path, str)

assert Path(cert_path).exists()
assert "BEGIN RSA PRIVATE KEY" in Path(cert_path).read_text(encoding="utf-8")
Expand Down Expand Up @@ -328,6 +329,7 @@ def test_resolve_cert_from_destination_with_base64_bundle(
transport._tmp_key_file = None

cert_path = transport._resolve_cert()
assert isinstance(cert_path, str)

assert Path(cert_path).exists()
assert "BEGIN CERTIFICATE" in Path(cert_path).read_text(encoding="utf-8")
Expand Down Expand Up @@ -373,7 +375,7 @@ def test_decode_destination_certificate_content_rejects_missing_key(self) -> Non

def test_resolve_cert_without_config_raises(self) -> None:
transport = object.__new__(HttpTransport)
transport._config = types.SimpleNamespace(
transport._config = types.SimpleNamespace( # ty: ignore[invalid-assignment]
cert=None,
key=None,
cert_path=None,
Expand Down
6 changes: 3 additions & 3 deletions tests/destination/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ def destination_client():
client = create_client()
return client
except Exception as e:
pytest.fail(f"Failed to create Destination client for cloud integration tests: {e}")
pytest.fail(f"Failed to create Destination client for cloud integration tests: {e}") # ty: ignore[invalid-argument-type]


@pytest.fixture(scope="session")
Expand All @@ -43,7 +43,7 @@ def fragment_client():
client = create_fragment_client()
return client
except Exception as e:
pytest.fail(f"Failed to create Fragment client for cloud integration tests: {e}")
pytest.fail(f"Failed to create Fragment client for cloud integration tests: {e}") # ty: ignore[invalid-argument-type]


@pytest.fixture(scope="session")
Expand All @@ -56,7 +56,7 @@ def certificate_client():
client = create_certificate_client()
return client
except Exception as e:
pytest.fail(f"Failed to create Certificate client for cloud integration tests: {e}")
pytest.fail(f"Failed to create Certificate client for cloud integration tests: {e}") # ty: ignore[invalid-argument-type]


@pytest.fixture
Expand Down
8 changes: 4 additions & 4 deletions tests/destination/integration/test_destination_bdd.py
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ def use_configured_subscriber_tenant(context):
"""Set the tenant from the CLOUD_SDK_CFG_DESTINATION_DEFAULT_TENANT_SUBDOMAIN environment variable."""
tenant = os.environ.get("CLOUD_SDK_CFG_DESTINATION_DEFAULT_TENANT_SUBDOMAIN")
if not tenant:
pytest.skip("CLOUD_SDK_CFG_DESTINATION_DEFAULT_TENANT_SUBDOMAIN environment variable not set")
pytest.skip("CLOUD_SDK_CFG_DESTINATION_DEFAULT_TENANT_SUBDOMAIN environment variable not set") # ty: ignore[too-many-positional-arguments]
context.tenant = tenant


Expand Down Expand Up @@ -1616,12 +1616,12 @@ def send_get_request(context, path):
try:
context.http_response = context.http_client.request("GET", path)
except (requests.exceptions.ConnectionError, requests.exceptions.Timeout) as e:
pytest.skip(f"External endpoint unreachable — skipping: {e}")
pytest.skip(f"External endpoint unreachable — skipping: {e}") # ty: ignore[too-many-positional-arguments]

# skip if the echo service itself returned an error
if not context.http_response.ok:
pytest.skip(
f"External endpoint returned {context.http_response.status_code}: "
f"External endpoint returned {context.http_response.status_code}: " # ty: ignore[too-many-positional-arguments]
f"{context.http_response.text[:200]}"
)

Expand All @@ -1639,7 +1639,7 @@ def assert_authorization_header_present(context):
def call_get_service_instance_id(context, destination_client):
"""Call get_service_instance_id and store the result."""
if not os.environ.get("CLOUD_SDK_CFG_DESTINATION_DEFAULT_INSTANCEID"):
pytest.skip("CLOUD_SDK_CFG_DESTINATION_DEFAULT_INSTANCEID is not set — skipping service instance ID test")
pytest.skip("CLOUD_SDK_CFG_DESTINATION_DEFAULT_INSTANCEID is not set — skipping service instance ID test") # ty: ignore[too-many-positional-arguments]
context.service_instance_id = destination_client.get_service_instance_id()


Expand Down
4 changes: 2 additions & 2 deletions tests/dms/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ def dms_client():
client = create_client(instance="default")
return client
except Exception as e:
pytest.skip(f"DMS integration tests require credentials: {e}")
pytest.skip(f"DMS integration tests require credentials: {e}") # ty: ignore[too-many-positional-arguments]


@pytest.fixture(scope="session", autouse=True)
Expand Down Expand Up @@ -52,7 +52,7 @@ def _setup_test_repositories(dms_client):
)
created_repos.append(repo.id)
except DMSError as e:
pytest.skip(f"DMS ECM repository connection not available — skipping DMS integration tests: {e}")
pytest.skip(f"DMS ECM repository connection not available — skipping DMS integration tests: {e}") # ty: ignore[too-many-positional-arguments]

yield

Expand Down
2 changes: 1 addition & 1 deletion tests/dms/integration/test_dms_bdd.py
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ def select_version_repo(context: DMSTestContext, dms_client: DMSClient):
version_repo = r
break
if version_repo is None:
pytest.skip("No version-enabled repository available")
pytest.skip("No version-enabled repository available") # ty: ignore[too-many-positional-arguments]
context.repo = version_repo
context.repo_id = version_repo.id

Expand Down
6 changes: 3 additions & 3 deletions tests/objectstore/integration/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def integration_env() -> Dict[str, str]:
missing_vars.append(var)

if missing_vars:
pytest.skip(f"Missing required environment variables for cloud integration tests: {missing_vars}")
pytest.skip(f"Missing required environment variables for cloud integration tests: {missing_vars}") # ty: ignore[too-many-positional-arguments]

# Ensure SSL is enabled for cloud services
env_vars["CLOUD_SDK_CFG_OBJECTSTORE_DEFAULT_SSL_ENABLED"] = os.getenv(
Expand All @@ -58,7 +58,7 @@ def integration_env() -> Dict[str, str]:
# Validate that we're not using localhost (cloud-only)
host = env_vars["CLOUD_SDK_CFG_OBJECTSTORE_DEFAULT_HOST"]
if host.startswith("localhost") or host.startswith("127.0.0.1"):
pytest.skip("Integration tests are cloud-only. Local endpoints not supported.")
pytest.skip("Integration tests are cloud-only. Local endpoints not supported.") # ty: ignore[too-many-positional-arguments]

logger.info(f"Integration environment validated for cloud testing: {host}")
return env_vars
Expand All @@ -78,7 +78,7 @@ def objectstore_client(integration_env):
client = create_client("default", config=config, disable_ssl=disable_ssl)
return client
except Exception as e:
pytest.fail(f"Failed to create ObjectStore client for cloud integration tests: {e}")
pytest.fail(f"Failed to create ObjectStore client for cloud integration tests: {e}") # ty: ignore[invalid-argument-type]


@pytest.fixture
Expand Down
Loading
Loading