Skip to main content

Typed Python client for the SAP Cloud for Utilities Foundation Measurement Concept Management (MCM) APIs

Project description

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: MCMAPIErrorMCMValidationError (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: 2xxINFO, 4xxWARNING, 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


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sap_mcm_client-0.0.3.tar.gz (118.6 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sap_mcm_client-0.0.3-py3-none-any.whl (57.2 kB view details)

Uploaded Python 3

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

Hashes for sap_mcm_client-0.0.3.tar.gz
Algorithm Hash digest
SHA256 060151bbe36b57b217057cfc32ad8c10b603e67e72151bebbe691135dcacff55
MD5 c2a05559c5f0e91b13b085434fe62f24
BLAKE2b-256 b329ebe9a8e62af0ec4b3f9488d5c18762221fd9dd13cd2aaaeb2d712a0201e5

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

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

Hashes for sap_mcm_client-0.0.3-py3-none-any.whl
Algorithm Hash digest
SHA256 26c42b88aa8b6a428d66df6cc938441ac89e70713bf57154ab1c2cb9fe0cf7a2
MD5 bab2faf5769397a3b89ddec7841999ed
BLAKE2b-256 2e998a1c2b1be12f9427cff616b5ee2575170e4d610ed18db2f4b336114bf46d

See more details on using hashes here.

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

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page