sap-mcm-client

module
v0.0.4 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Jul 27, 2026 License: MIT

README

sap-mcm-client

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.

Directories

Path Synopsis
OAuth2 Client Credentials authentication for the SAP MCM API client.
OAuth2 Client Credentials authentication for the SAP MCM API client.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL