Skip to main content

sap-mcm-client

License: MIT PyPI version Supported Python versions Go Reference Go Report Card

Python Tests Python Coverage Python Lint Python Formatting Go Tests Go Coverage Go Lint

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):

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 $batch requests 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

uv sync --group dev
uv run pytest unittests                    # pytest
uv run pylint sap_mcm_client                # pylint (10/10 required)
uv run mypy --strict src/sap_mcm_client     # mypy --strict
uv run coverage run -m pytest unittests && uv run coverage report --fail-under 80 --omit "unittests/*"  # coverage >= 80%
uv run codespell --ignore-words=domain-specific-terms.txt src README.md  # codespell
uv run black . && uv run 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.

Metadata

Release files for sap-mcm-client 0.0.4

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for sap-mcm-client 0.0.4
File Size Uploaded
sap_mcm_client-0.0.4.tar.gz 210.6 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for sap-mcm-client 0.0.4
File Interpreter ABI Platform
sap_mcm_client-0.0.4-py3-none-any.whl Python 3 none any Details

Total release size: 267.7 kB

Release files / sap_mcm_client-0.0.4.tar.gz

Download URL sap_mcm_client-0.0.4.tar.gz
Size 210.6 kB
Tags Source
SHA-256 checksum
How to use checksums
ba41bdbe70892c8e11106678977c85a7cdb109dc106a21f2255fb4487a69f2c6
BLAKE2b-256 checksum
How to use checksums
a048e7d1482cd1d5f1ad2a704bb486eea62d70f7cdade5ba65769a18a4f93310
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Release files / sap_mcm_client-0.0.4-py3-none-any.whl

Download URL sap_mcm_client-0.0.4-py3-none-any.whl
Size 57.1 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ee2b376ca710e90908b8516496dcb01f3d6aae8a26ab34d702db55d66515b59b
BLAKE2b-256 checksum
How to use checksums
5f9023f38ca7f1a4106b92c78e9b618ee4d47e27c96488fc4fbc2960662843da
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/6.1.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Jul 27, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.0.4 This release

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page