seventhings Python SDK
Python client for the seventhings Customer API (/customer-api/v1). It offers the same features as the Go and PHP SDKs (v1.4.0) and comes with both a synchronous and an asynchronous client.
- Python 3.10+
- The only runtime dependency is
httpx - Fully typed (
py.typed), with dataclass models
Installation
pip install seventhings-customer-api
# or
uv add seventhings-customer-api
The import name is seventhings.
Quick Start
Password authentication
from seventhings import Client
with Client.with_credentials(
"https://example.seventhings.com", "user@example.com", "password", "client-id"
) as client:
print(client.objects.count())
Pre-existing token
client = Client("https://example.seventhings.com", token="my-jwt-token")
Manual login and refresh
client = Client("https://example.seventhings.com")
tok = client.auth.login("user@example.com", "password", "client-id") # stores the access token
tok = client.auth.refresh(tok.refresh_token) # reuses the client ID from login
client.auth.revoke_tokens()
SSO authentication
from seventhings.models import SSOAppTarget, SSOProviderName
tok = client.auth.login_sso(SSOProviderName.AZURE, auth_code, "client-id", SSOAppTarget.WEB)
Async
AsyncClient has the same API as Client, except that its methods are coroutines, the all() iterators are async iterators, and you close it with aclose() or async with:
from seventhings import AsyncClient
async with await AsyncClient.with_credentials(url, user, password, client_id) as client:
obj = await client.objects.get(uuid)
async for room in client.rooms.all():
print(room.name)
Configuration
Client(
base_url, # instance URL; "/customer-api/v1" is appended
token=None, # bearer token
client_id=None, # OAuth client ID used by auth.refresh()
http_client=None, # your own httpx.Client / httpx.AsyncClient (not closed by the SDK)
timeout=30.0, # seconds; None disables the timeout
)
Usage
Ping
ping = client.ping() # unauthenticated
print(ping.status) # "OK"
Objects
Objects have a schema that differs per tenant, so they are plain dicts. The all() iterator yields Fields, a dict with typed accessors.
from seventhings.models import ListOptions, SortDirection, like
uuid = client.objects.create({"inventory_name": "Laptop", "barcode": "INV-001"})
obj = client.objects.get(uuid)
obj = client.objects.get_by_barcode("INV-001") # archived objects included
client.objects.patch(uuid, {"inventory_name": "Laptop (IT)"})
client.objects.archive(uuid)
client.objects.unarchive(uuid)
client.objects.delete(uuid)
page = client.objects.list(ListOptions(page=1, per_page=50).where(like("inventory_name", "Laptop")))
total = client.objects.count()
for obj in client.objects.all(ListOptions(per_page=100)): # walks every page
print(obj.uuid, obj.get_str("inventory_name"), obj.get_time("updated_at"))
To attach a file you have already uploaded to an attachment field, use add_files. It returns the raw Response, because the API can answer 207 Multi-Status:
from seventhings.models import FileAttachment
resp = client.objects.add_files(uuid, [FileAttachment("documents", file_uuid)])
client.objects.remove_files(uuid, [FileAttachment("documents", file_uuid)])
History
Objects, rooms, locations, persons, tasks and rental cases all have a paged history. The API defaults to page 1 with 50 entries per page (maximum 200).
from seventhings.models import HistoryListOptions
hist = client.persons.history(person_uuid, HistoryListOptions(page=1, per_page=20))
for entry in hist.items:
print(entry.occurred_at, entry.event_name, entry.details) # details is a JSON string
if hist.page * hist.per_page < hist.total:
... # fetch the next page
Object history entries are plain dicts, because their shape depends on type (asset, task, rental_case or object_merge).
PDF reports
from seventhings.models import CreateReport
templates = client.reports.list_templates()
pdf: bytes = client.reports.create(CreateReport(templates[0].uuid, [obj_uuid]))
Files
with open("photo.jpg", "rb") as fh:
file_uuid = client.files.upload("photo.jpg", fh) # or pass bytes
meta = client.files.get(file_uuid)
data = client.files.get_data(file_uuid)
thumb = client.files.get_thumbnail(file_uuid)
recent = client.files.list() # the most recent files (max 20)
Tasks
from seventhings.models import (
CreateTask,
TaskListOptions,
TaskReferenceInput,
TaskStatus,
TimeInterval,
TimeIntervalUnit,
UpdateTask,
)
task_uuid = client.tasks.create(
CreateTask(
title="Inspect",
deadline="2026-12-31",
assignees=[user_uuid],
references=[TaskReferenceInput(obj_uuid)],
reminders=[TimeInterval(TimeIntervalUnit.DAYS, 1)],
)
)
tasks = client.tasks.list(TaskListOptions(status=TaskStatus.OPEN))
client.tasks.update_status(task_uuid, TaskStatus.CLOSED)
client.tasks.update(task_uuid, UpdateTask(title="Inspect again", assignees=[user_uuid])) # PUT
client.tasks.delete(task_uuid)
Rental cases
from seventhings.models import (
CreateRentalCase,
RentalCaseReferenceInput,
RentalCaseRenter,
RenterType,
)
rental_uuid = client.rentals.create(
CreateRentalCase(
title="Laptop loan",
renter=RentalCaseRenter(RenterType.USER, user_uuid),
references=[RentalCaseReferenceInput(obj_uuid)],
issue_date="2026-01-01 09:00:00",
due_date="2026-01-08 09:00:00",
responsible_user_uuid=user_uuid,
)
)
rental = client.rentals.get(rental_uuid)
for rc in client.rentals.all():
print(rc.title, rc.status)
Rooms and locations
These work like objects. The difference is that patch returns the updated record, and responses that come back as a {uuid, fields} envelope are flattened into a single dict for you.
loc_uuid = client.locations.create({"name": "HQ"})
room_uuid = client.rooms.create({"name": "Office 1", "building_id": 1})
room = client.rooms.patch(room_uuid, {"name": "Office 1a"})
Users and persons
from seventhings.models import PersonListOptions, UserListOptions, UserSortBy, UserSortOrder
users = client.users.list(UserListOptions(sort_by=UserSortBy.EMAIL, order=UserSortOrder.ASC))
me = client.users.get_by_id(tok.user_id)
person_uuid = client.persons.create({"email": "ada@example.com", "first_name": "Ada"})
person = client.persons.get(person_uuid)
print(person.email, person.fields.get_str("cost_center")) # every field is in person.fields
client.persons.patch(person_uuid, {"department": "IT"})
for p in client.persons.all(PersonListOptions(sort_by="last_name")):
...
Field definitions
from seventhings.models import AssetTrackingTemplate
defs = client.field_definitions.list(AssetTrackingTemplate.ASSET)
required = client.field_definitions.mandatory(AssetTrackingTemplate.ASSET)
missing = client.field_definitions.missing_mandatory_fields(AssetTrackingTemplate.ASSET, payload)
options = defs[0].field_type.allowed_values() # dropdown values
Circularity Hub
Items and orders in the Circularity Hub use integer IDs.
from seventhings.models import AddObjectEntry, FilterObject, FilterOperator
suggestions = client.circularity_hub.suggest_category(
FilterObject(filter={"uuid": {FilterOperator.IN: [obj_uuid]}})
) # None when there are no suggestions
client.circularity_hub.add_objects({obj_uuid: AddObjectEntry("chairs", "25.00")})
for item in client.circularity_hub.all_items():
...
order_id = client.circularity_hub.create_order([1, 2])
order = client.circularity_hub.get_order(order_id)
Raw requests
request() covers anything the SDK doesn't wrap:
resp = client.request("GET", "objects", query="page=1&per_page=5")
print(resp.status_code, resp.json())
Filtering and sorting
from seventhings.models import ListOptions, SortDirection, gte, in_, like
opts = (
ListOptions(page=1, per_page=50)
.sort_by("name", SortDirection.ASC)
.sort_by("created_at", SortDirection.DESC)
.where(like("name", "Laptop"), in_("status", "active", "pending"), gte("price", "100"))
)
This produces the following query string (brackets are sent literally and only the values are escaped):
page=1&per_page=50&sort[name]=ASC&sort[created_at]=DESC&filter[name][like][]=Laptop&filter[status][in][]=active&filter[status][in][]=pending&filter[price][gte]=100
| Operator | Helper | Description |
|---|---|---|
eq |
eq |
Equal |
neq |
neq |
Not equal |
gt, gte |
gt, gte |
Greater than (or equal) |
gt_or_null, gte_or_null |
gt_or_null, gte_or_null |
Greater than (or equal), including null |
lt, lte |
lt, lte |
Less than (or equal) |
lt_or_null, lte_or_null |
lt_or_null, lte_or_null |
Less than (or equal), including null |
like |
like |
Contains substring (multi-value) |
not_like |
not_like |
Does not contain substring (multi-value) |
in |
in_ |
Value in set (multi-value) |
nin |
nin |
Value not in set (multi-value) |
Error handling
Every exception derives from seventhings.SeventhingsError:
| Exception | When |
|---|---|
APIError |
The API returned a status of 400 or higher |
NetworkError |
Connection failure, timeout or another transport error (wraps the httpx exception) |
DecodeError |
The response could not be decoded, e.g. invalid JSON or a missing Location header |
from seventhings import APIError
try:
client.objects.get("nonexistent")
except APIError as err:
print(err.status_code, err.body)
if err.is_not_found: # also: is_unauthorized, is_forbidden, is_conflict,
... # is_rate_limited, is_server_error, is_feature_inactive
is_feature_inactive is true when the endpoint's module (for example rentals) is not active on the instance.
Pagination
list() methods fetch a single page. The all() iterators on objects, rooms, locations, rentals, users, persons and Circularity Hub items (all_items()) walk through every page:
- they ignore
opts.page; - they use
opts.per_pageas the page size, defaulting to 100; - they stop at the first page that is shorter than the page size.
Tasks and files have no paging. History is paged manually.
Scope and limitations
- No automatic token refresh. Call
client.auth.refresh(refresh_token)yourself when the access token expires. - No automatic retry and no rate limiting. Wrap calls yourself if you need either, or pass an
httpxclient that uses a retrying transport. - Dates are strings, as the API sends them (
Y-m-d H:i:s, UTC).Fields.get_time()parses them. - Unknown enum values from newer API versions are kept as plain strings rather than raising an error.
Development
uv sync
uv run pytest # unit tests; each one runs against both Client and AsyncClient
uv run ruff check . && uv run ruff format --check .
uv run mypy # strict
uv run python scripts/unasync.py # regenerate the sync client after editing _async/client.py
The synchronous client (src/seventhings/_sync/client.py) is generated from src/seventhings/_async/client.py, so edit the async file and regenerate. CI runs scripts/unasync.py --check. Request building and response decoding live in src/seventhings/_operations.py, which does no I/O and is shared by both clients.
Integration tests
The integration tests run against a live instance and create, then delete, their own test data:
cp .env.example .env # fill in SEVENTHINGS_BASE_URL/USERNAME/PASSWORD/CLIENT_ID
scripts/run-integration.sh # or: uv run pytest -m integration
scripts/run-integration.sh -k person # forward pytest args
examples/demo/ walks through the SDK end to end and uses the same environment variables.
License
MIT
Release files for seventhings-customer-api 1.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| seventhings_customer_api-1.4.0.tar.gz | 93.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| seventhings_customer_api-1.4.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 133.5 kB
Release files / seventhings_customer_api-1.4.0.tar.gz
| Download URL | seventhings_customer_api-1.4.0.tar.gz |
|---|---|
| Size | 93.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ee6fce732fa8c5a9d0780708bbd2142df444e60ef9994ddc6aff28e5e1f2feb7
|
|
BLAKE2b-256 checksum How to use checksums |
1724f9fa2dc6c23616cc2bcbbdfe39f026e24686393e40334016588a871c5eac
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.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 Sep 27, 2026.
Transparency logRelease files / seventhings_customer_api-1.4.0-py3-none-any.whl
| Download URL | seventhings_customer_api-1.4.0-py3-none-any.whl |
|---|---|
| Size | 40.5 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
dd81c010076b1f2f42efa3aa06afd5ef1b196c4ae1d53085f3f624515d868f56
|
|
BLAKE2b-256 checksum How to use checksums |
29aaa3cf182115e4948f6f3c974401fb1d568e7408d5c68e982dc21e419578d5
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.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 Sep 27, 2026.
Transparency log