Skip to content

Commit a6dd461

Browse files
authored
Merge branch 'main' into docs/readme-missing-modules
2 parents 7af1d6f + fb2cce5 commit a6dd461

7 files changed

Lines changed: 214 additions & 202 deletions

File tree

.env_integration_tests.example

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,5 @@ CLOUD_SDK_CFG_DESTINATION_DEFAULT_IDENTITYZONE=your-identity-zone-here
1717
CLOUD_SDK_CFG_SDM_DEFAULT_URI=https://your-sdm-api-uri-here
1818
CLOUD_SDK_CFG_SDM_DEFAULT_UAA='{"url":"https://your-auth-url","clientid":"your-client-id","clientsecret":"your-client-secret","identityzone":"your-identity-zone"}'
1919

20-
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL=https://your-agent-memory-api-url-here
21-
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_URL=https://your-auth-url-here
22-
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTID=your-client-id-here
23-
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTSECRET=your-client-secret-here
20+
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL=https://your-agent-memory-api-url-here
21+
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA='{"url":"https://your-auth-url","clientid":"your-client-id","clientsecret":"your-client-secret"}'

README.md

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ The Python SDK offers a clean, type-safe API following Python best practices whi
3030

3131
### Installation
3232

33-
3433
#### uv
3534

3635
```bash
@@ -47,7 +46,7 @@ poetry add sap-cloud-sdk
4746

4847
```bash
4948
pip install sap-cloud-sdk
50-
````
49+
```
5150

5251
### Environment Configuration
5352

docs/INTEGRATION_TESTS.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -70,10 +70,8 @@ For Agent Memory integration tests, configure the following variables in `.env_i
7070

7171
```bash
7272
# Agent Memory Configuration
73-
CLOUD_SDK_CFG_AGENT_MEMORY_DEFAULT_URL=https://your-agent-memory-api-url
74-
CLOUD_SDK_CFG_AGENT_MEMORY_DEFAULT_AUTH_URL=https://your-auth-url
75-
CLOUD_SDK_CFG_AGENT_MEMORY_DEFAULT_CLIENTID=your-client-id
76-
CLOUD_SDK_CFG_AGENT_MEMORY_DEFAULT_CLIENTSECRET=your-client-secret
73+
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL=https://your-agent-memory-api-url
74+
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA='{"url":"https://your-auth-url","clientid":"your-client-id","clientsecret":"your-client-secret"}'
7775
```
7876

7977
## Running Integration Tests

src/sap_cloud_sdk/agent_memory/config.py

Lines changed: 57 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,21 @@
55
66
Mount path convention::
77
8+
/etc/secrets/appfnd/hana-agent-memory/default/url
9+
/etc/secrets/appfnd/hana-agent-memory/default/uaa
810
11+
``url`` is the Agent Memory service base URL (plain string).
12+
``uaa`` is a JSON string with OAuth2 credentials containing at minimum:
13+
``clientid``, ``clientsecret``, and ``url`` (UAA base URL).
914
10-
/etc/secrets/appfnd/hana-agent-memory/default/{field_key}
15+
Env fallback convention::
1116
12-
Keys: ``application_url``, ``uaa.url``, ``uaa.clientid``, ``uaa.clientsecret``
13-
14-
Env fallback convention (uppercased)::
15-
16-
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL
17-
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_URL
18-
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTID
19-
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTSECRET
17+
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL
18+
CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA
2019
"""
2120

22-
from dataclasses import dataclass, field
21+
import json
22+
from dataclasses import dataclass
2323
from typing import Optional
2424

2525
from sap_cloud_sdk.agent_memory.exceptions import AgentMemoryConfigError
@@ -60,11 +60,18 @@ class AgentMemoryConfig:
6060
def __post_init__(self) -> None:
6161
if not self.base_url:
6262
raise AgentMemoryConfigError("base_url must be a non-empty string")
63-
64-
65-
# NOTE: BindingData must NOT use `from __future__ import annotations`
66-
# because the secret resolver checks `f.type is str` at runtime, which requires
67-
# actual type objects rather than string annotations.
63+
if self.token_url is not None and not self.token_url:
64+
raise AgentMemoryConfigError(
65+
"token_url must be a non-empty string when provided"
66+
)
67+
if self.client_id is not None and not self.client_id:
68+
raise AgentMemoryConfigError(
69+
"client_id must be a non-empty string when provided"
70+
)
71+
if self.client_secret is not None and not self.client_secret:
72+
raise AgentMemoryConfigError(
73+
"client_secret must be a non-empty string when provided"
74+
)
6875

6976

7077
@dataclass
@@ -74,73 +81,42 @@ class BindingData:
7481
All fields must be plain ``str`` to satisfy the resolver contract.
7582
"""
7683

77-
application_url: str = ""
78-
uaa_url: str = field(default="", metadata={"secret": "uaa.url"})
79-
uaa_clientid: str = field(default="", metadata={"secret": "uaa.clientid"})
80-
uaa_clientsecret: str = field(default="", metadata={"secret": "uaa.clientsecret"})
84+
url: str = ""
85+
uaa: str = ""
8186

8287
def validate(self) -> None:
8388
"""Raise ``AgentMemoryConfigError`` if any required field is empty."""
84-
missing = [
85-
f
86-
for f in ("application_url", "uaa_url", "uaa_clientid", "uaa_clientsecret")
87-
if not getattr(self, f)
88-
]
89-
if missing:
89+
if not self.url:
90+
raise AgentMemoryConfigError(
91+
"Agent Memory binding is missing required field: url"
92+
)
93+
if not self.uaa:
9094
raise AgentMemoryConfigError(
91-
f"Agent Memory binding is missing required fields: {', '.join(missing)}"
95+
"Agent Memory binding is missing required field: uaa"
9296
)
9397

9498
def extract_config(self) -> AgentMemoryConfig:
95-
"""Derive an ``AgentMemoryConfig`` from the raw binding fields."""
96-
return AgentMemoryConfig(
97-
base_url=self.application_url,
98-
token_url=self.uaa_url.rstrip("/") + "/oauth/token",
99-
client_id=self.uaa_clientid,
100-
client_secret=self.uaa_clientsecret,
101-
)
102-
103-
104-
_ENV_PREFIX = "CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT"
105-
106-
# Explicit env var names — dots are not valid in shell variable names,
107-
# so we define these directly rather than deriving them from BindingData metadata keys
108-
# (which use dots to match the BTP mount-path file naming convention).
109-
_ENV_VARS = {
110-
"application_url": f"{_ENV_PREFIX}_APPLICATION_URL",
111-
"uaa_url": f"{_ENV_PREFIX}_UAA_URL",
112-
"uaa_clientid": f"{_ENV_PREFIX}_UAA_CLIENTID",
113-
"uaa_clientsecret": f"{_ENV_PREFIX}_UAA_CLIENTSECRET",
114-
}
115-
116-
117-
def _load_binding_from_env() -> BindingData:
118-
"""Read Agent Memory binding from environment variables.
119-
120-
Raises:
121-
AgentMemoryConfigError: If any required variable is absent.
122-
"""
123-
import os
124-
125-
binding = BindingData()
126-
missing: list[str] = []
127-
for attr, var in _ENV_VARS.items():
128-
value = os.environ.get(var)
129-
if not value:
130-
missing.append(var)
131-
else:
132-
setattr(binding, attr, value)
133-
if missing:
134-
raise AgentMemoryConfigError(
135-
f"Missing required environment variables: {', '.join(missing)}"
136-
)
137-
return binding
99+
"""Parse the UAA JSON string and return an ``AgentMemoryConfig``."""
100+
try:
101+
uaa_data = json.loads(self.uaa, strict=False)
102+
except json.JSONDecodeError as e:
103+
raise AgentMemoryConfigError(f"Failed to parse uaa JSON: {e}")
104+
105+
try:
106+
return AgentMemoryConfig(
107+
base_url=self.url,
108+
token_url=uaa_data["url"].rstrip("/") + "/oauth/token",
109+
client_id=uaa_data["clientid"],
110+
client_secret=uaa_data["clientsecret"],
111+
)
112+
except KeyError as e:
113+
raise AgentMemoryConfigError(f"Missing required field in uaa JSON: {e}")
138114

139115

140116
def _load_config_from_env() -> AgentMemoryConfig:
141117
"""Load Agent Memory configuration from a mounted volume or environment variables.
142118
143-
Tries (in order):
119+
Uses the secret resolver with fallback order:
144120
1. Mount at ``/etc/secrets/appfnd/hana-agent-memory/default/``
145121
2. Environment variables ``CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_*``
146122
@@ -150,24 +126,24 @@ def _load_config_from_env() -> AgentMemoryConfig:
150126
Raises:
151127
AgentMemoryConfigError: If configuration cannot be loaded or is incomplete.
152128
"""
153-
from sap_cloud_sdk.core.secret_resolver.resolver import _load_from_mount
129+
from sap_cloud_sdk.core.secret_resolver import (
130+
read_from_mount_and_fallback_to_env_var,
131+
)
154132

155-
mount_error: Exception | None = None
156133
try:
157134
binding = BindingData()
158-
_load_from_mount("/etc/secrets/appfnd", "hana-agent-memory", "default", binding)
159-
binding.validate()
160-
return binding.extract_config()
161-
except Exception as exc:
162-
mount_error = exc
163-
164-
try:
165-
binding = _load_binding_from_env()
135+
read_from_mount_and_fallback_to_env_var(
136+
base_volume_mount="/etc/secrets/appfnd",
137+
base_var_name="CLOUD_SDK_CFG",
138+
module="hana-agent-memory",
139+
instance="default",
140+
target=binding,
141+
)
166142
binding.validate()
167143
return binding.extract_config()
168144
except AgentMemoryConfigError:
169145
raise
170146
except Exception as exc:
171147
raise AgentMemoryConfigError(
172-
f"Failed to load Agent Memory configuration: mount={mount_error}; env={exc}"
148+
f"Failed to load Agent Memory configuration: {exc}"
173149
) from exc

src/sap_cloud_sdk/agent_memory/user-guide.md

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -772,14 +772,37 @@ environment variables or service binding.
772772
773773
## Configuration
774774
775-
`create_client()` resolves credentials automatically in the following order:
775+
### Service Binding
776776
777-
1. **Mounted volume**`/etc/secrets/appfnd/hana-agent-memory/default/{field}`
778-
2. **Environment variables**`CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_*`
777+
- **Mount path**: `$SERVICE_BINDING_ROOT/hana-agent-memory/default/` (defaults to `/etc/secrets/appfnd/hana-agent-memory/default/`)
778+
- **Required keys**: `url` (Agent Memory service URL), `uaa` (JSON string with XSUAA credentials)
779+
- **Env var fallback**: `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_{FIELD}` (uppercased)
779780
780-
| Environment Variable | Description |
781-
| ---------------------------------------------------------- | ------------------------------------ |
782-
| `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL` | Base URL of the Agent Memory service |
783-
| `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_URL` | OAuth2 authorization server base URL |
784-
| `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTID` | OAuth2 client ID |
785-
| `CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTSECRET` | OAuth2 client secret |
781+
> **Note:** `SERVICE_BINDING_ROOT` defaults to `/etc/secrets/appfnd` when not set. See the [Secret Resolver guide](../core/secret_resolver/user-guide.md) for details.
782+
783+
#### Mounted Secrets (Kubernetes)
784+
785+
```
786+
$SERVICE_BINDING_ROOT/hana-agent-memory/default/
787+
├── url
788+
└── uaa
789+
```
790+
791+
#### Environment Variables
792+
793+
```bash
794+
export CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL="https://agent-memory.example.com"
795+
export CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA='{"clientid":"...","clientsecret":"...","url":"https://..."}'
796+
```
797+
798+
#### UAA JSON Schema
799+
800+
The `uaa` key must contain a JSON string with the XSUAA credentials:
801+
802+
```json
803+
{
804+
"clientid": "sb-xxx",
805+
"clientsecret": "xxx",
806+
"url": "https://subdomain.authentication.region.hana.ondemand.com"
807+
}
808+
```

tests/agent_memory/unit/test_client.py

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -45,10 +45,13 @@ def test_uses_provided_config(self):
4545

4646
def test_reads_env_when_no_config_provided(self, monkeypatch):
4747
"""Factory falls back to environment variables when no config given."""
48-
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_APPLICATION_URL", "http://memory.example.com")
49-
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_URL", "http://auth.example.com")
50-
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTID", "client-id")
51-
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA_CLIENTSECRET", "client-secret")
48+
import json
49+
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_URL", "http://memory.example.com")
50+
monkeypatch.setenv("CLOUD_SDK_CFG_HANA_AGENT_MEMORY_DEFAULT_UAA", json.dumps({
51+
"url": "http://auth.example.com",
52+
"clientid": "client-id",
53+
"clientsecret": "client-secret",
54+
}))
5255
with patch("sap_cloud_sdk.agent_memory.HttpTransport") as MockTransport:
5356
MockTransport.return_value = MagicMock(spec=HttpTransport)
5457
client = create_client()

0 commit comments

Comments
 (0)