Typed Python client for the SAP Cloud for Utilities Foundation Measurement Concept Management (MCM) APIs
Project description
sap-mcm-client
Typed Python and Go client for the SAP Cloud for Utilities Foundation Measurement Concept Management (MCM) OData V4 APIs.
What this does
Provides typed models and an HTTP client that hides the OData V4 protocol behind a clean, domain-specific interface. Instead of constructing raw OData queries with $expand, $filter, and $select, you work with typed Python (Pydantic v2) or Go structs.
Status
Alpha. The type definitions are derived from the SAP MCM OpenAPI specs (v1.1.0) and have not yet been validated against a live SAP system.
Supported APIs
All five APIs of the SAP Cloud for Utilities Foundation package:
| API | Python | Go | Operations | SAP docs |
|---|---|---|---|---|
| Measurement Concept Instance | ✅ | ✅ | CRUD + 4 lifecycle actions + 5 sub-entity updates + 3 notifications | API guide · reference |
| Measurement Concept Class | ✅ | ✅ | Read-only (list + get) | reference |
| Measurement Concept Model | ✅ | ✅ | Read-only (list + get) | reference |
| Instance Migration | ✅ | ✅ | Batch import: migrate + get + list staged + purge + check progress | API guide |
| Time Series | ✅ | ✅ | 12 read variants + 2 upload + 3 delete | reference |
Deeper background on the MCM domain itself (Messkonzeptklasse, Messkonzeptmodell, Messkonzeptinstanz):
- SAP Utilities Core Foundation — Measurement Concept Management (SAP Help Portal)
- MCM component overview on community.sap.com
For a condensed tour of the entity hierarchy and OData conventions, see docs/SPECS_ANALYSIS.md. The SAP OpenAPI specs themselves are not redistributed in this repository (they're SAP IP); see CONTRIBUTING.md for how to download them locally.
Installation
Python
pip install sap-mcm-client
PyPI project page: pypi.org/project/sap-mcm-client
Go
go get github.com/Hochfrequenz/sap-mcm-client/mcm
Module / API docs: pkg.go.dev/github.com/Hochfrequenz/sap-mcm-client/mcm
Quickstart
Python
The Python client is async (built on aiohttp); call it from within an
event loop and await each operation.
import asyncio
from sap_mcm_client import MCMClient, Division, OverallStatus
async def main() -> None:
async with MCMClient(
base_url="https://c4u-foundation-mcm-service.cfapps.eu10.hana.ondemand.com",
token_url="https://mysubaccount.authentication.eu10.hana.ondemand.com/oauth/token",
client_id="...",
client_secret="...",
) as client:
# List instances with typed filters — no OData query strings needed
instances = await client.instances.list(
division=Division.ELECTRICITY,
overall_status=OverallStatus.ACTIVE,
top=50,
)
for instance in instances.items:
print(f"{instance.id_text}: {instance.description}")
# Fetch one instance with full expansion
instance = await client.instances.get(
"01234567-89ab-cdef-0123-456789abcdef",
include=["all"],
)
for metering_location in instance.metering_locations:
for task in metering_location.metering_tasks:
print(task.register_code)
# List classes and models
classes = await client.classes.list(division=Division.ELECTRICITY)
models = await client.models.list(include=["market_locations"])
asyncio.run(main())
Go
package main
import (
"context"
"fmt"
"log"
"github.com/Hochfrequenz/sap-mcm-client/mcm"
)
func main() {
client := mcm.NewClient(mcm.Config{
BaseURL: "https://c4u-foundation-mcm-service.cfapps.eu10.hana.ondemand.com",
Auth: mcm.AuthConfig{
TokenURL: "https://mysubaccount.authentication.eu10.hana.ondemand.com/oauth/token",
ClientID: "...",
ClientSecret: "...",
},
})
ctx := context.Background()
// List instances
top := 50
instances, err := client.Instances.List(ctx, &mcm.ListOptions{
Top: &top,
Filter: map[string]string{"division_code": "EL", "overallStatus_code": "ACTIVE"},
})
if err != nil {
log.Fatal(err)
}
for _, inst := range instances.Items {
description := ""
if inst.Description != nil {
description = *inst.Description
}
fmt.Printf("%s: %s\n", inst.IDText, description)
}
// Fetch one instance with full expansion (expansion is automatic on Get)
inst, err := client.Instances.Get(ctx, "01234567-89ab-cdef-0123-456789abcdef")
if err != nil {
if mcm.IsNotFound(err) {
log.Fatal("instance not found")
}
log.Fatal(err)
}
fmt.Println(len(inst.MeteringLocations), "metering locations")
}
OAuth2 Configuration
The client authenticates against SAP BTP using the OAuth2 Client Credentials flow. You need four values from your SAP subaccount's service binding:
| Value | Example | Where to find it |
|---|---|---|
base_url |
https://c4u-foundation-mcm-service.cfapps.eu10.hana.ondemand.com |
Service binding url (in some regions replace eu10 with ap10) |
token_url |
https://<subaccount>.authentication.eu10.hana.ondemand.com/oauth/token |
Service binding uaa.url + /oauth/token |
client_id |
sb-xsuaa-xxxxx!b12345|mcm-service!b67890 |
Service binding uaa.clientid |
client_secret |
<generated secret> |
Service binding uaa.clientsecret |
Recommended: store credentials in environment variables and load them via python-dotenv (Python) or os.Getenv (Go). Never commit credentials to the repo.
For the underlying administration details (service instance provisioning, role collections, JWT scopes), see SAP's Administration Guide for the MCM Component.
Limitations
Be honest about what this client can and can't do today:
- Not yet validated against a live SAP system. All models are derived from the OpenAPI specs downloaded from api.sap.com on 2026-04-13. The real API may have undocumented fields, different error formats, or additional enum values.
- Test fixtures are spec-derived, not recorded from real responses. A recording script will close this gap in a future version.
- Enum values may be incomplete. The specs list known codes, but the real system may accept additional values. All enums are typed strings so unknown values still deserialize correctly.
- No batch support yet. OData
$batchrequests for atomic multi-entity updates are not implemented.
Error handling
Python
from sap_mcm_client import MCMClient, MCMNotFoundError, MCMForbiddenError
try:
instance = await client.instances.get("some-uuid")
except MCMNotFoundError:
print("Instance does not exist")
except MCMForbiddenError as e:
print(f"Access denied: {e.detail}")
Full exception hierarchy: MCMAPIError → MCMValidationError (400), MCMAuthenticationError (401), MCMForbiddenError (403), MCMNotFoundError (404). MCMAuthError is raised separately when OAuth2 token acquisition fails.
Go
inst, err := client.Instances.Get(ctx, "some-uuid")
if err != nil {
switch {
case mcm.IsNotFound(err):
fmt.Println("instance does not exist")
case mcm.IsForbidden(err):
fmt.Println("access denied")
default:
log.Fatal(err)
}
}
Logging
The Python client emits one structured "wide event" per outbound request —
a single canonical log line carrying high-cardinality context as key-value
fields, rather than several fragmented messages. This follows the
wide-event / canonical-log-line approach and uses
only the standard library logging module.
The library never configures logging itself (a NullHandler is attached to the
sap_mcm_client logger); your application owns handlers, formatters, and levels.
Each API request logs once on the sap_mcm_client logger with these fields
attached via the record's extra:
| Field | Example | Notes |
|---|---|---|
event |
"mcm.request" |
event name (mcm.token_fetch for OAuth2 token fetches) |
request_id |
"9f8c…" |
unique per request (high cardinality) |
http_method |
"GET" |
|
url |
".../MCMInstances" |
request path; query parameters are not logged |
http_status |
200 |
|
duration_ms |
42.7 |
wall-clock duration |
response_bytes |
1834 |
|
ok |
true |
2xx |
error_type, error |
"ClientConnectionError" |
only on failures |
The level reflects the outcome so errors always surface even when the happy
path is quiet: 2xx → INFO, 4xx → WARNING, 5xx and transport failures →
ERROR. Credentials are never logged (no bearer token, client secret, or
request headers).
To get JSON wide events, point the sap_mcm_client logger at a structured
handler — for example with python-json-logger:
import logging
from pythonjsonlogger.json import JsonFormatter
handler = logging.StreamHandler()
handler.setFormatter(JsonFormatter())
log = logging.getLogger("sap_mcm_client")
log.addHandler(handler)
log.setLevel(logging.INFO)
Each request then emits a single JSON object with all of the fields above. Cost control (tail sampling — keep all errors/slow requests, sample the happy path) is best applied in your logging pipeline or collector, since the decision is made on the event's outcome.
The Go client mirrors this with the standard library log/slog. Pass a
*slog.Logger via mcm.Config{Logger: ...}; when omitted, logging is disabled
(no output). Each request emits one record with the same fields
(event, request_id, http_method, url, http_status, duration_ms,
response_bytes, ok) and the same level-by-outcome mapping, and OAuth2 token
fetches emit a redaction-safe mcm.token_fetch event.
import (
"log/slog"
"os"
"github.com/Hochfrequenz/sap-mcm-client/mcm"
)
client := mcm.NewClient(mcm.Config{
BaseURL: "https://...",
Auth: mcm.AuthConfig{ /* ... */ },
Logger: slog.New(slog.NewJSONHandler(os.Stderr, nil)),
})
Development
Python
pip install -e ".[tests,linting,type_check,formatting]"
tox -e tests # pytest
tox -e linting # pylint (10/10 required)
tox -e type_check # mypy --strict
tox -e coverage # coverage >= 80%
tox -e spell_check # codespell
black . && isort . # auto-format
Go
go test ./...
golangci-lint run --enable dupl,goconst,gocyclo
Contributing
See CONTRIBUTING.md for the workflow when updating types from new spec versions, and CLAUDE.md for conventions used throughout the codebase.
License
MIT — see LICENSE.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file sap_mcm_client-0.0.3.tar.gz.
File metadata
- Download URL: sap_mcm_client-0.0.3.tar.gz
- Upload date:
- Size: 118.6 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
060151bbe36b57b217057cfc32ad8c10b603e67e72151bebbe691135dcacff55
|
|
| MD5 |
c2a05559c5f0e91b13b085434fe62f24
|
|
| BLAKE2b-256 |
b329ebe9a8e62af0ec4b3f9488d5c18762221fd9dd13cd2aaaeb2d712a0201e5
|
Provenance
The following attestation bundles were made for sap_mcm_client-0.0.3.tar.gz:
Publisher:
python-publish.yml on Hochfrequenz/sap-mcm-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sap_mcm_client-0.0.3.tar.gz -
Subject digest:
060151bbe36b57b217057cfc32ad8c10b603e67e72151bebbe691135dcacff55 - Sigstore transparency entry: 1815062001
- Sigstore integration time:
-
Permalink:
Hochfrequenz/sap-mcm-client@2b971279fe2b9d5833b21d4b71e7cf9ecb9be0c9 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/Hochfrequenz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@2b971279fe2b9d5833b21d4b71e7cf9ecb9be0c9 -
Trigger Event:
release
-
Statement type:
File details
Details for the file sap_mcm_client-0.0.3-py3-none-any.whl.
File metadata
- Download URL: sap_mcm_client-0.0.3-py3-none-any.whl
- Upload date:
- Size: 57.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
26c42b88aa8b6a428d66df6cc938441ac89e70713bf57154ab1c2cb9fe0cf7a2
|
|
| MD5 |
bab2faf5769397a3b89ddec7841999ed
|
|
| BLAKE2b-256 |
2e998a1c2b1be12f9427cff616b5ee2575170e4d610ed18db2f4b336114bf46d
|
Provenance
The following attestation bundles were made for sap_mcm_client-0.0.3-py3-none-any.whl:
Publisher:
python-publish.yml on Hochfrequenz/sap-mcm-client
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
sap_mcm_client-0.0.3-py3-none-any.whl -
Subject digest:
26c42b88aa8b6a428d66df6cc938441ac89e70713bf57154ab1c2cb9fe0cf7a2 - Sigstore transparency entry: 1815062130
- Sigstore integration time:
-
Permalink:
Hochfrequenz/sap-mcm-client@2b971279fe2b9d5833b21d4b71e7cf9ecb9be0c9 -
Branch / Tag:
refs/tags/v0.0.3 - Owner: https://github.com/Hochfrequenz
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
python-publish.yml@2b971279fe2b9d5833b21d4b71e7cf9ecb9be0c9 -
Trigger Event:
release
-
Statement type: