Visitar URL original
feat(cbc): add CBC (Central Business Configuration) client module by soumyadey · Pull Request #333 · SAP/cloud-sdk-python · GitHub
Skip to content

feat(cbc): add CBC (Central Business Configuration) client module - #333

Open
soumyadey wants to merge 19 commits into
SAP:mainfrom
soumyadey:feat/cbc-client
Open

soumyadey wants to merge 19 commits into
SAP:mainfrom
soumyadey:feat/cbc-client

Conversation

@soumyadey

@soumyadey soumyadey commented Sep 14, 2026 •

Copy link
Copy Markdown
Member

Description

Adds sap_cloud_sdk.cbc — a typed Python client for reading tenant-specific business configuration from SAP Central Business Configuration (CBC). The client is built as two layers: a generic core (client.py) where base_url and app_tenant_id are per-request callables and mTLS is supplied as a Callable[[], ssl.SSLContext] factory, and a platform adapter (client_adapter.py) that ships the SAP application-platform provisioning defaults on top of the core. The mTLS certificate is reloaded automatically when a request fails the TLS handshake (certificate rotation/expiry), so a long-lived client recovers without being recreated. Includes a full exception hierarchy, plain-dataclass models, and a CBCClient Protocol for test doubles.

Related Issue

Closes #280

Type of Change

  • New feature (non-breaking change that adds functionality)

How to Test

Unit tests (no external service required):

pytest tests/cbc/unit/

Integration tests (requires a CBC server or mock):

Against a plain-HTTP mock (CLOUD_SDK_CBC_URL must have the CBC tenant id baked in — it is used verbatim):

CLOUD_SDK_CBC_URL=http://localhost:8001 \
CLOUD_SDK_CBC_APP_TENANT_ID=<app-tenant-id> \
pytest -v tests/cbc/integration/

Against a real mTLS server, add the client cert and key paths:

CLOUD_SDK_CBC_URL=https://<cbc-tenant-id>.<rest-of-host> \
CLOUD_SDK_CBC_APP_TENANT_ID=<app-tenant-id> \
CLOUD_SDK_CBC_CERT_PATH=<path-to-cert.pem> \
CLOUD_SDK_CBC_KEY_PATH=<path-to-key.pem> \
pytest -v tests/cbc/integration/

CLOUD_SDK_CBC_URL, CLOUD_SDK_CBC_CERT_PATH, and CLOUD_SDK_CBC_KEY_PATH are integration-harness env vars (read only by the test conftest), not part of the SDK's public API. The cert/key are optional — supply both for mTLS, omit for a plain-HTTP mock.

Expected result: 64 unit tests pass; integration tests skip automatically when env vars are absent (CI-safe).

Checklist

  • I have read the Contributing Guidelines
  • I have verified that my changes solve the issue
  • I have added/updated automated tests to cover my changes
  • All tests pass locally
  • I have verified that my code follows the Code Guidelines
  • I have updated documentation (if applicable)
  • I have added type hints for all public APIs
  • My code does not contain sensitive information (credentials, tokens, etc.)
  • I have followed Conventional Commits for commit messages

Breaking Changes

None. This is a new module with no existing public API.

Additional Notes

Module structure follows the repo convention (client.py, client_adapter.py, exceptions.py, _models.py, py.typed, user-guide.md).

Key design decisions:

  • Two-layer contract: the core create_client is generic and knows nothing about the platform — base_url and app_tenant_id are Callable[[], str] invoked per request (both vary per tenant in a multi-tenant agent), and the credential input is a Callable[[], ssl.SSLContext] factory. The platform adapter (create_agent_client) layers strictly on top and supplies the SAP application-platform defaults.
  • The adapter owns two ContextVars (app_tenant_id_var, tenant_subdomain_var) that the app populates per request; it resolves base_url from the tenant-mapping Destination Fragment (listing the subaccount and matching on appTenantId) and loads the provider-level mTLS certificate from the Destination Service. Every default is overridable via the CBCDestinationConfig object passed to create_agent_client, or CLOUD_SDK_CBC_* env vars.
  • create_agent_client takes a single CBCDestinationConfig settings object (which Destination Service instance, certificate name, and keystore password to read) rather than loose keyword args — the config object lives in cbc/config.py, matching the dedicated-config.py convention of the agw, adms, print, and destination modules. A value set on the config wins over its CLOUD_SDK_CBC_* env var, which in turn falls back to the platform default.
  • mTLS certificate rotation is handled reactively: the ssl_context factory is resolved once at construction and re-invoked only when a request fails the TLS handshake (an expired/rotated client cert surfaces as ReadError wrapping ssl.SSLError, detected by walking the exception cause chain). On such a failure the client rebuilds its httpx.Client from a fresh context under a lock and retries the request once; a second failure, a non-TLS transport error, or a failing rebuild propagates cleanly, leaving the previous working client in place. A long-lived create_agent_client() singleton therefore recovers from rotation on its own. Mirrors the objectstore _execute_with_retry rotation pattern.
  • The three platform resolvers (resolve_base_url, resolve_app_tenant_id, load_ssl_context) are public, exported at the top level. create_agent_client stays a fixed preset wiring all three; a caller who wants to keep most of the preset but override a single axis composes create_client with the public resolvers plus their own callable for that axis, instead of reimplementing the resolvers.
  • get_configuration reads the config objects from the CBC configurationObjects API — which already returns each config object with its child entities — so consumers work with the authored config-object vocabulary without any client-side grouping. The per-request base_url / app_tenant_id callables are each resolved once per public call and threaded into the internal requests, so a single get_configuration triggers one base_url resolution (one tenant-mapping fragment lookup in the adapter), not one per entity. Entity data is read from the confirmed API shape: contentShape drives whether content.item (OBJECT) or content.items (ARRAY) is used; entity_id comes from the URL path directly (confirmed equal to metadata.entityId).
  • get_entity_data(config_object_id, entity_id, consumption_version=None) is a targeted single-entity HTTP call, matching the Java SDK's getEntityData(). It avoids the N+1 HTTP calls get_configuration makes (one per entity across all config objects) when only one entity is needed. When consumption_version is None, the latest version is resolved automatically (2 HTTP calls total); when pinned, only 1. ConfigObject.get_entity_data(entity_id) and ConfigData.get_entity_data(config_object_id, entity_id) provide the same lookup in-memory on an already-fetched result, returning EntityData | None directly (no .data unwrap needed).
  • ConfigEntity, EntityData, ConfigObject, ConfigData are plain @dataclass (not Pydantic) — they are constructed in client code, never parsed from JSON.
  • Only the public API methods carry @record_metrics; internal helpers do not, to avoid double-counting a single user operation.

Test evidence:

64 passed, 1 warning
tests/cbc/unit/test_client.py::TestAppTenantIdCallable::test_app_tenant_id_callable_on_each_call_invokes_callable PASSED
tests/cbc/unit/test_client.py::TestConfigurationsUrl::test_configurations_url_with_base_and_suffix_joins_correctly PASSED
tests/cbc/unit/test_client.py::TestGetConsumptionVersions::test_get_consumption_versions_with_valid_response_returns_parsed_versions PASSED
tests/cbc/unit/test_client.py::TestGetConsumptionVersions::test_get_consumption_versions_on_404_raises_cbc_client_error PASSED
tests/cbc/unit/test_client.py::TestGetConsumptionVersions::test_get_consumption_versions_on_500_raises_cbc_server_error PASSED
tests/cbc/unit/test_client.py::TestGetConsumptionVersions::test_get_consumption_versions_on_connection_failure_raises_cbc_network_error PASSED
tests/cbc/unit/test_client.py::TestGetConfigurationObjects::test_get_configuration_objects_with_multiple_objects_returns_grouped_config_objects PASSED
tests/cbc/unit/test_client.py::TestFetchEntityData::test_fetch_entity_data_with_array_shape_returns_list_content PASSED
tests/cbc/unit/test_client.py::TestFetchEntityData::test_fetch_entity_data_with_object_shape_returns_dict_content PASSED
tests/cbc/unit/test_client.py::TestFetchEntityData::test_fetch_entity_data_with_absent_content_defaults_to_empty_list PASSED
tests/cbc/unit/test_client.py::TestGetConfiguration::test_get_configuration_without_version_resolves_latest_and_returns_config_data PASSED
tests/cbc/unit/test_client.py::TestGetConfiguration::test_get_configuration_when_no_versions_exist_raises_cbc_client_error PASSED
tests/cbc/unit/test_client.py::TestGetConfiguration::test_get_configuration_with_explicit_version_skips_version_resolution PASSED
tests/cbc/unit/test_client.py::TestGetConfiguration::test_get_configuration_with_multiple_entities_resolves_base_url_once PASSED
tests/cbc/unit/test_client.py::TestGetEntityData::test_get_entity_data_without_version_resolves_latest_and_returns_entity_data PASSED
tests/cbc/unit/test_client.py::TestGetEntityData::test_get_entity_data_with_pinned_version_skips_version_resolution PASSED
tests/cbc/unit/test_client.py::TestGetEntityData::test_get_entity_data_when_no_versions_exist_raises_cbc_client_error PASSED
tests/cbc/unit/test_client.py::TestDefaultClientContextManager::test_context_manager_on_exit_closes_http_client PASSED
tests/cbc/unit/test_client.py::TestCreateClient::test_create_client_with_required_args_returns_default_client PASSED
tests/cbc/unit/test_client.py::TestCreateClient::test_create_client_with_ssl_context_passes_it_through_to_default_client PASSED
tests/cbc/unit/test_client.py::TestIsTlsFailure::test_is_tls_failure_with_read_error_wrapping_ssl_error_returns_true PASSED
tests/cbc/unit/test_client.py::TestIsTlsFailure::test_is_tls_failure_with_connect_error_returns_false PASSED
tests/cbc/unit/test_client.py::TestIsTlsFailure::test_is_tls_failure_with_non_ssl_os_error_returns_false PASSED
tests/cbc/unit/test_client.py::TestCertRotation::test_cert_rotation_on_tls_failure_rebuilds_client_and_retries_successfully PASSED
tests/cbc/unit/test_client.py::TestCertRotation::test_cert_rotation_on_second_tls_failure_after_rebuild_propagates_error PASSED
tests/cbc/unit/test_client.py::TestCertRotation::test_cert_rotation_on_non_tls_transport_error_does_not_rebuild_client PASSED
tests/cbc/unit/test_client.py::TestCertRotation::test_cert_rotation_without_ssl_factory_does_not_rebuild_client PASSED
tests/cbc/unit/test_client.py::TestCertRotation::test_cert_rotation_when_factory_fails_propagates_error_and_retains_old_client PASSED
tests/cbc/unit/test_client_adapter.py::TestResolveAppTenantId::test_resolve_app_tenant_id_with_set_contextvar_returns_value PASSED
tests/cbc/unit/test_client_adapter.py::TestResolveAppTenantId::test_resolve_app_tenant_id_with_empty_contextvar_raises_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestResolveBaseUrl::test_resolve_base_url_with_matching_fragment_returns_cbc_url PASSED
tests/cbc/unit/test_client_adapter.py::TestResolveBaseUrl::test_resolve_base_url_with_empty_subdomain_raises_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestResolveBaseUrl::test_resolve_base_url_with_empty_app_tenant_id_raises_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestResolveBaseUrl::test_resolve_base_url_with_no_matching_fragment_raises_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestResolveBaseUrl::test_resolve_base_url_with_fragment_missing_cbc_url_raises_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestResolveBaseUrl::test_resolve_base_url_when_destination_raises_wraps_as_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestLoadSslContext::test_load_ssl_context_with_valid_cert_returns_ssl_context PASSED
tests/cbc/unit/test_client_adapter.py::TestLoadSslContext::test_load_ssl_context_when_cert_not_found_raises_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestLoadSslContext::test_load_ssl_context_when_destination_raises_wraps_as_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestCreateAgentClient::test_create_agent_client_with_landscape_env_builds_ssl_context_from_cert PASSED
tests/cbc/unit/test_client_adapter.py::TestCreateAgentClient::test_create_agent_client_with_env_overrides_applies_instance_and_cert_name PASSED
tests/cbc/unit/test_client_adapter.py::TestCreateAgentClient::test_create_agent_client_with_config_object_applies_instance_and_cert_name PASSED
tests/cbc/unit/test_client_adapter.py::TestCreateAgentClient::test_create_agent_client_with_config_and_env_set_config_value_wins PASSED
tests/cbc/unit/test_client_adapter.py::TestCreateAgentClient::test_create_agent_client_without_landscape_or_cert_name_raises_cbc_config_error PASSED
tests/cbc/unit/test_client_adapter.py::TestCreateAgentClient::test_create_agent_client_with_contextvars_set_wires_resolvers_into_client PASSED
tests/cbc/unit/test_client_adapter.py::TestComposeWithPublicResolvers::test_compose_core_with_public_resolvers_builds_valid_default_client PASSED
tests/cbc/unit/test_models.py::TestConsumptionVersionsLatest::test_latest_with_empty_list_returns_none PASSED
tests/cbc/unit/test_models.py::TestConsumptionVersionsLatest::test_latest_with_modified_dates_returns_most_recently_modified PASSED
tests/cbc/unit/test_models.py::TestConsumptionVersionsLatest::test_latest_without_modified_dates_returns_most_recently_created PASSED
tests/cbc/unit/test_models.py::TestConsumptionVersionsLatest::test_latest_without_any_dates_returns_last_item PASSED
tests/cbc/unit/test_models.py::TestEntityData::test_as_list_with_list_content_returns_list PASSED
tests/cbc/unit/test_models.py::TestEntityData::test_as_list_with_dict_content_raises_value_error PASSED
tests/cbc/unit/test_models.py::TestEntityData::test_as_object_with_dict_content_returns_dict PASSED
tests/cbc/unit/test_models.py::TestEntityData::test_as_object_with_list_content_raises_value_error PASSED
tests/cbc/unit/test_models.py::TestEntityData::test_is_list_with_list_content_returns_true_and_is_object_returns_false PASSED
tests/cbc/unit/test_models.py::TestEntityData::test_is_object_with_dict_content_returns_true_and_is_list_returns_false PASSED
tests/cbc/unit/test_models.py::TestEntityData::test_value_with_any_content_returns_raw_without_shape_assertion PASSED
tests/cbc/unit/test_models.py::TestConfigData::test_get_config_object_with_matching_id_returns_config_object PASSED
tests/cbc/unit/test_models.py::TestConfigData::test_get_config_object_with_missing_id_returns_none PASSED
tests/cbc/unit/test_models.py::TestConfigData::test_get_entity_data_with_matching_ids_returns_correct_entity_data PASSED
tests/cbc/unit/test_models.py::TestConfigData::test_get_entity_data_with_missing_entity_id_returns_none PASSED
tests/cbc/unit/test_models.py::TestApiError::test_from_response_with_cbc_error_envelope_parses_code_and_message PASSED
tests/cbc/unit/test_models.py::TestApiError::test_from_response_with_empty_body_falls_back_to_unknown_error PASSED
tests/cbc/unit/test_models.py::TestApiError::test_from_response_with_unparseable_body_falls_back_to_unknown_error PASSED

64 passed, 1 warning
Integration: 5 passed in 19.10s (real CBC server)
platform darwin -- Python 3.12.12, pytest-9.1.0
plugins: asyncio-1.4.0, bdd-8.1.0, respx-0.23.1

tests/cbc/integration/test_e2e_bdd.py::test_consumption_versions_non_empty PASSED
tests/cbc/integration/test_e2e_bdd.py::test_latest_version_non_empty PASSED
tests/cbc/integration/test_e2e_bdd.py::test_get_configuration_returns_config_data PASSED
tests/cbc/integration/test_e2e_bdd.py::test_configuration_has_config_objects PASSED
tests/cbc/integration/test_e2e_bdd.py::test_every_entity_has_id_and_data PASSED

5 passed, 1 warning in 19.10s
Sample ConfigData response (real CBC server)
{
  "consumption_version": "a0392d4f-...",
  "app_tenant_id": "<app-tenant-id>",
  "config_objects": [
    {
      "config_object_id": "payment-config",
      "entities": [
        {
          "entity_id": "payment-mode",
          "data": [
            { "paymentModeCode": "CASH",          "name": "Cash",                "isOnline": false },
            { "paymentModeCode": "CARD",          "name": "Credit / Debit Card", "isOnline": false },
            { "paymentModeCode": "DIGITAL_WALLET","name": "Digital Wallet",      "isOnline": true  }
          ]
        }
      ]
    },
    {
      "config_object_id": "tax-config",
      "entities": [
        {
          "entity_id": "tax-category",
          "data": [
            { "code": "STD",     "ratePercent": 8.5, "isDefault": true  },
            { "code": "REDUCED", "ratePercent": 5,   "isDefault": false },
            { "code": "ZERO",    "ratePercent": 0,   "isDefault": false }
          ]
        }
      ]
    }
  ]
}

@soumyadey
soumyadey requested a review from a team as a code owner September 14, 2026 15:10
Comment thread src/sap_cloud_sdk/cbc/client.py Outdated
Comment thread src/sap_cloud_sdk/cbc/client.py Outdated
Comment thread src/sap_cloud_sdk/cbc/client.py Outdated
Comment thread src/sap_cloud_sdk/cbc/config.py Outdated
@soumyadey
soumyadey force-pushed the feat/cbc-client branch 2 times, most recently from c3dc18a to c62dfa8 Compare September 18, 2026 17:41
@soumyadey
soumyadey force-pushed the feat/cbc-client branch 2 times, most recently from 814ef60 to 4dd6e00 Compare October 6, 2026 05:01
Typed Python client for reading tenant-specific business configuration
from SAP Central Business Configuration. Supports mTLS (production),
local/mock (loopback auto-detection), and HTTPS mock servers via the
CLOUD_SDK_CBC_REPLACE_SUBDOMAIN env var override.

Public API: create_client(), CBCClient protocol, DefaultClient,
CBCConfig, ConfigData / ConfigObject / EntityData / EntityContent,
ConsumptionVersions, and a full CBC exception hierarchy.
…h params

Replace the cert tuple parameter with symmetric cert_path/key_path params.
Add CLOUD_SDK_CBC_CERT / CLOUD_SDK_CBC_KEY env vars so PEM values can be
supplied directly (e.g. from K8s secrets) without writing to disk first —
create_client() handles the temp-file lifecycle automatically.
…nts, version bump

- Bump version to 0.54.0 (required by CI for src/ changes)
- Fix ruff format violations in _models.py and client.py
- Fix ty errors: conftest fixture return type CBCClient, test_models assert-not-None before .version
- Update test_module (15→16) and test_operation (161→163) counts for CBC module/operations
- Soften "do not instantiate" to "prefer create_client"
- Replace contradictory direct-instantiation examples with create_client usage
- Reference BTP Destination Service and env vars as credential sources
- Add tmp/ to .gitignore
`_is_local_url` auto-disabled subdomain replacement for loopback URLs.
Replace with an explicit opt-out: `replace_subdomain` now defaults to
`True`; consumers set `CLOUD_SDK_CBC_REPLACE_SUBDOMAIN=false` when
pointing at a local mock server.

- Remove `_is_local_url` from `_http.py`
- Default `replace_subdomain` to `True` in `DefaultClient.__init__`
- Update `config.py` and `user-guide.md` to document the env-var escape hatch
- Remove `TestDefaultClientLocalMode` and related tests
Binds `TenantContext | Callable[[], TenantContext]` at construction time
instead of per-call. The callable form supports multi-tenant agents where
the tenant varies per request (e.g. read from a request-scoped context var).

- `DefaultClient.__init__` and `create_client` gain `tenant_context` param
- `get_consumption_versions` and `get_configuration` drop the param
- `CBCClient` Protocol updated to match
- Integration conftest bakes tenant into the client fixture
- Tests cover callable invocation count and missing-tenant error
Redesign the CBC client around a generic core and a platform adapter layered
strictly on top of it.

Core (client.py): base_url and app_tenant_id are per-request callables; the only
credential input is an ssl.SSLContext. create_client is a thin factory. Drops
TenantContext, config.py, _http.py, and all env/cert-file machinery.

Platform adapter (client_adapter.py): ships the SAP application-platform
provisioning defaults. The SDK owns two ContextVars (app_tenant_id_var,
tenant_subdomain_var) that the app populates; create_agent_client resolves
base_url from the tenant-mapping Destination Fragment (listing the subaccount
and matching on appTenantId, pre-PR-SAP#79 shape) and loads the provider mTLS
cert from the Destination Service. Every default is overridable via args or
CLOUD_SDK_CBC_* env vars.

CBC unit coverage 98% (adapter 100%).
get_configuration previously re-invoked the base_url callable on every
HTTP request (consumption versions + config objects + one per entity),
so a single call triggered 2+N tenant-mapping fragment lookups in the
platform adapter. Resolve base_url once at the top of the public call
and thread the resolved string into the internal methods
(_get_configuration_objects, _fetch_entity_data, _configurations_url),
mirroring how app_tenant_id is already threaded. One operation now does
one base_url resolution.

Also fix incoherent unit-test data: agent-config no longer holds
restaurant/contact/hours entities; replaced with tax-config /
tax-category / tax-rate to match the finance-domain examples used
elsewhere.
The adapter's _resolve_base_url and _load_ssl_context call Destination
Service APIs that can raise DestinationError, which leaked past the
documented CBCConfigError contract. Wrap both in try/except, re-raising
as CBCConfigError with a descriptive message and chained cause.
The adapter loaded the mTLS certificate once in create_agent_client and
baked it into the httpx.Client at construction, so a rotated or expired
certificate killed a long-lived client until the process restarted —
violating the "Credential Binding Rotation" guideline that every module
reading credentials must recover from rotation.

Make the core reload reactively. ssl_context becomes a
Callable[[], ssl.SSLContext] | None, resolved once at construction and
re-invoked only when a request fails the TLS handshake. On such a
failure the client rebuilds its httpx.Client from a fresh context under
a lock (guarding against a thundering herd) and retries the one request
once; a second failure, a non-TLS transport error, or a failing rebuild
(the cert loader raises CBCConfigError) propagates cleanly, leaving the
previous working client in place. TLS failures are detected by walking
the exception's __cause__/__context__ chain for ssl.SSLError, since httpx
surfaces an expired client cert as ReadError wrapping ssl.SSLError two
levels down. Mirrors objectstore's _execute_with_retry rotation pattern.

create_agent_client drops its ssl_context parameter and now owns cert
resolution end-to-end, passing a lambda: load_ssl_context(...) factory to
the core.

Expose the three platform resolvers as public composition helpers
(resolve_base_url, resolve_app_tenant_id, load_ssl_context) at the top
level, so a caller can keep most of the platform preset but override a
single axis via create_client instead of reimplementing the resolvers.

Also:
- resolve base_url + app_tenant_id once per get_configuration on the
  auto-version path (extract _get_consumption_versions taking resolved
  values), avoiding a double resolve that could also disagree if the
  request context changed between the two.
- rename _build_client/_rebuild_client/self._client to the _http_client
  forms, so names disambiguate the httpx client from the CBC client.
- build the client with verify=ctx if ctx is not None else True, making
  it explicit that TLS verification is never disabled.
The pre-commit ty hook checks tests/ (unlike an src-only ty run), which
surfaced 10 diagnostics in the CBC test suite.

test_client_adapter.py: create_agent_client() returns the CBCClient
Protocol, which has no private members, so accessing _ssl_factory /
_base_url / _resolve_app_tenant_id failed. Narrow to DefaultClient with
an isinstance assert before touching privates, and guard the optional
_ssl_factory before calling it.

test_client.py: orig_build = client._build_http_client is typed
() -> httpx.Client, so orig_build() was Client, not MagicMock, breaking
the .request wiring and the -> MagicMock return annotations. Align the
build helpers' return types with the method they replace (httpx.Client),
cast() the mock where .request is wired, and replace the ineffective
mypy # type: ignore[method-assign] with the repo's ty idiom
# ty: ignore[invalid-assignment].
Replace the three loose keyword args on create_agent_client
(destination_instance, cbc_cert_name, p12_password) with a single
CBCDestinationConfig settings object, addressing the PR review request
for a config object. The object lives in a new cbc/config.py, matching
the dedicated-config.py convention of the agw, adms, print, and
destination modules.

Per-field env overrides are unchanged: a value set on the config wins
over its CLOUD_SDK_CBC_* env var, which in turn falls back to the
platform default. Scoped to the adapter layer only — the core
create_client and its hardcoded timeout are untouched; a core config
object is deferred.

Also rename the private _ClientConfig (API-path holder) to _ApiPaths so
it no longer reads as a near-homonym of the new public config class.
EntityContent (the raw payload wrapper) → EntityData: names the thing it
actually holds.
EntityData (the entity container with entity_id + data) → ConfigEntity:
scoped to the config layer, mirrors get_config_object naming, and
removes ambiguity with the new EntityData.

Also rename the two accessor methods for consistency:
  ConfigObject.get_entity       → get_config_entity
  ConfigData.get_entity_data    → get_config_entity

Updated across _models.py, client.py, __init__.py, user-guide.md, and
test_models.py. No behaviour change.
REST API team confirmed:
- metadata.entityId is the correct field name (was entityName)
- content.item / content.items struct is correct (already was)
- ?appTenantId= query param is correct (already was)

Since metadata.entityId equals the entity_id already known from the URL
path, drop the metadata read entirely — entity_id is used directly.
Remove the now-redundant resolved_id fallback and the deleted
test_falls_back_to_path_entity_id_without_metadata test. Strip stale
metadata fields from remaining mock response bodies.
… to 0.59.0

Fix stale EntityData return annotation on TestConfigData._entity_data —
missed during the EntityData → ConfigEntity rename. Also bump version to
0.59.0 (upstream released 0.58.0).
Exposes _fetch_entity_data as a public client method get_entity_data(),
matching the Java SDK's getEntityData() for fetching one entity without
pulling all configuration objects. Also renames ConfigObject/ConfigData
.get_config_entity() → .get_entity_data() returning EntityData directly,
removing the ConfigEntity wrapper the caller had to unwrap every time.
- Add Args/Returns/Raises to all three CBCClient Protocol methods
  (get_consumption_versions, get_configuration, get_entity_data)
- Rename all test methods across test_client.py, test_models.py, and
  test_client_adapter.py to the documented test_<func>_<condition>_<result>
  pattern (pre-existing violations + 3 new from the prior commit)
- Strengthen ConfigData.get_entity_data test: fixture now gives each entity
  distinct data so a wrong-entity selection bug would fail the assertion
- Add BDD integration scenario for get_entity_data (CONTRIBUTING.md requires
  integration tests for new capabilities)
…AP#82

Fragment naming in the platform changed from CBC_TenantMapping_<cbcTenantId>
to CBC_TenantMapping_<tenantSubdomain> (sap-internal-sdk-python#82), enabling
a direct get_subaccount_fragment lookup by subdomain instead of listing all
fragments and filtering by appTenantId.

Update resolve_base_url to use the single direct lookup, and update all five
TestResolveBaseUrl unit tests to mock get_subaccount_fragment accordingly.
from sap_cloud_sdk import cbc

cbc_client = cbc.create_client(
base_url=lambda: resolve_cbc_url(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't this be optional and based on SPII? Where user should get this url from?

cbc_client = cbc.create_client(
base_url=lambda: resolve_cbc_url(),
app_tenant_id=lambda: resolve_app_tenant_id(),
ssl_context=lambda: build_ssl_ctx(),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We usually just require the user_token on function level. We already have methods to facilitate the retrieve of it using runtime context module.

Returns:
A configured :class:`DefaultClient`.
"""
return DefaultClient(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
return DefaultClient(
return CBCClient(

# ---------------------------------------------------------------------------


def create_agent_client(

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not following other modules patterns. Usually we have an create_client which accepts a configuration parameter and in case this is not informed, we just infer information based on provisioning. Can you follow the same?

# -- Shared step state ---------------------------------------------------------


@pytest.fixture

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please share credentials in private so we can configure it in the repository.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: Add CBC (Central Business Configuration) consumption support

2 participants