Skip to main content

Twinetic API Client

A typed HTTP client for the Twinetic EMS REST API. Handles JWT authentication and token refresh transparently, and gives per-endpoint methods so you don't have to build URLs or parse pagination by hand.

Base of this client is the excellent HTTPX library: https://www.python-httpx.org/

Requirements

Python: >=3.12,<4.0

Transitive Dependencies:

Package Description
anyio High-level concurrency and networking framework on top of asyncio or Trio
certifi Python package for providing Mozilla's CA Bundle.
h11 A pure-Python, bring-your-own-I/O implementation of HTTP/1.1
httpcore A minimal low-level HTTP client.
httpx The next generation HTTP client.
idna Internationalized Domain Names in Applications (IDNA)

Installation

pip install twinetic-api-client

Authentication

The client authenticates with a refresh token — your personal access token (PAT), which you generate on your Twinetic EMS instance. The client exchanges it for short-lived access tokens automatically and re-mints them when they expire; you never handle access tokens yourself.

from twinetic.clients.ems.twinetic import TwineticClient

with TwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>") as client:
    meters = client.all_meters()

Use the client as a context manager (with) so the underlying connection is closed cleanly, or call client.close() yourself.

Reading data

Every list endpoint offers four access patterns, so you can choose how much data to fetch and how.

A single element by id. Fetch one record directly. Meter devices are looked up by UUID; measurements, units, prefixes, PQMs, and datapoints by their integer id.

from typing import Any
from twinetic.clients.ems.twinetic import TwineticClient

client = TwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>")

try:
    one_meter: dict[str, Any] = client.meter("fd7f55b5-4c8d-4b50-8cbb-6ceb95d04f5f")    # by UUID
    one_measurement: dict[str, Any] = client.single_measurement(40)                     # by id
finally:
    client.close()

A nonexistent id raises TwineticAPIError (404).

One page, with pagination info. Returns a RestfulResponse carrying count, next, previous, and results — reach for this when you want to page manually and need to know how many pages remain.

from twinetic.clients.dto import RestfulResponse
from twinetic.clients.ems.twinetic import TwineticClient

client = TwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>")

try:
    page: RestfulResponse = client.meters(page=1)
    print(page.count)      # total count of all meter units across all pages
    print(page.next)       # URL of the next page, or None at the end
    print(page.previous)   # URL of the previous page, or None at the beginning
    print(page.results)    # The entities of this page
finally:
    client.close()

All rows, eager. Fetches every page up front and returns them as one flat list. Every row is held in memory at once. Use this when you want the complete set and intend to keep it around.

from twinetic.clients.ems.twinetic import TwineticClient

client = TwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>")

try:
    for meter in client.all_meters():
        print(meter["name"])
finally:
    client.close()

All rows, lazy. Yields rows one page at a time, fetching each page only as you reach it — and only one page is held in memory at a time. Stop iterating early and the remaining pages are never fetched. Use this to stream large result sets or to search without pulling everything.

from twinetic.clients.ems.twinetic import TwineticClient

client = TwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>")

try:
    for meter in client.all_meters_lazy():
        if meter["name"] == "My special meter unit name":
            break   # only the pages needed to reach this row were fetched
finally:
    client.close()

The single-page method returns a RestfulResponse; the eager and lazy methods return the rows directly (a list[dict[str, Any]] and an iterator of dict[str, Any]). Rows are plain dictionaries — the same shape the API returns.

Choosing an API version

The client targets a default REST version set at construction (default_version, RESTVersion.V1 by default). Any single call can override it:

from typing import Any
from twinetic.clients.ems.twinetic import TwineticClient
from twinetic.clients.constants import RESTVersion

client = TwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>", default_version=RESTVersion.V1)

try:
    meters_v1: list[dict[str, Any]] = client.all_meters()                               # uses the default (v1) that was given during instantiation of the client
    meters_v2: list[dict[str, Any]] = client.all_meters(rest_version=RESTVersion.V2)    # this call uses v2
finally:
    client.close()

Asynchronous Clients

Every client has an async twin, e.g. TwineticClient & AsyncTwineticClient. The asynchronous variants expose the same methods, just awaited. Eager and single-element methods are awaited; the _lazy iterators are consumed with async for.

import asyncio

from twinetic.clients.ems.twinetic import AsyncTwineticClient


async def main() -> None:
    async with AsyncTwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>") as client:
        # eager and single-element: await
        for meter in await client.all_meters():
            print(meter["name"])

        one = await client.single_meter("d244eada-7e97-40cb-aeb2-e6183be7fe06")
        print(one["name"])

        # lazy: async for
        async for meter in client.all_meters_lazy():
            print(meter["name"])


asyncio.run(main())

Use the async client as an async context manager (async with) so the connection closes cleanly, or call await client.aclose() yourself.

Error handling

Any 4xx/5xx response raises TwineticAPIError, which carries the status code and the parsed response body:

from typing import Any
from twinetic.clients.ems.twinetic import TwineticClient
from twinetic.clients.exception import TwineticAPIError

client = TwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>")

try:
    # NOTE: 99th page does NOT exist in this example, therefore this call will raise an error
    client.units(99)
except TwineticAPIError as exc:
    print(exc.status_code)
    print(exc.body)
finally:
    client.close()

An expired or revoked refresh token raises RefreshTokenInvalidError — generate a new JWT refresh token on your Twinetic EMS instance and construct the client from new with it.

from twinetic.clients.ems.twinetic import TwineticClient
from twinetic.clients.exception import RefreshTokenInvalidError

client = TwineticClient(base_url="https://my-ems-instance.twinetic.de", refresh_token="<YOUR-GENERATED-REFRESH-TOKEN>")

try:
    client.all_meters()
except RefreshTokenInvalidError:
    print("Refresh token no longer valid — generate a new PAT and reconstruct the client.")
finally:
    client.close()

Download files

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

Source Distribution

twinetic_api_client-0.2.2.tar.gz (13.0 kB view details)

Uploaded Source

Built Distribution

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

twinetic_api_client-0.2.2-py3-none-any.whl (13.0 kB view details)

Uploaded Python 3

File details

Details for the file twinetic_api_client-0.2.2.tar.gz.

File metadata

  • Download URL: twinetic_api_client-0.2.2.tar.gz
  • Upload date:
  • Size: 13.0 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.12.14

File hashes

Hashes for twinetic_api_client-0.2.2.tar.gz
Algorithm Hash digest
SHA256 bc402220eca4c3fcbb7e7c55fc3e858946d6a3c048c591678eb764efc55be7e3
MD5 1a0bb726e8b96be4558ebbec9b0ed1aa
BLAKE2b-256 bad796f57039c9abfb39e0b26af38fe701de7ef5e395da34835ec4df99e54a72

See more details on using hashes here.

File details

Details for the file twinetic_api_client-0.2.2-py3-none-any.whl.

File metadata

File hashes

Hashes for twinetic_api_client-0.2.2-py3-none-any.whl
Algorithm Hash digest
SHA256 1dd90a716bdbf672b42c647bff986b65b3e1bc6a3bd09bf9023cc72f2361df0b
MD5 050afd2cb2533f969bd5de4ee92ae3ed
BLAKE2b-256 e4dc7598223abfd854fde10974967c6c2c59ef3aaed5632162dbb60959e800ee

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

0.2.2 This release

2 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