seventhings-customer-api 1.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- seventhings_customer_api-1.4.0/.env.example +7 -0
- seventhings_customer_api-1.4.0/.github/workflows/ci.yml +25 -0
- seventhings_customer_api-1.4.0/.github/workflows/release.yml +53 -0
- seventhings_customer_api-1.4.0/.gitignore +13 -0
- seventhings_customer_api-1.4.0/CLAUDE.md +63 -0
- seventhings_customer_api-1.4.0/LICENSE +21 -0
- seventhings_customer_api-1.4.0/PKG-INFO +400 -0
- seventhings_customer_api-1.4.0/README.md +371 -0
- seventhings_customer_api-1.4.0/examples/demo/README.md +174 -0
- seventhings_customer_api-1.4.0/examples/demo/demo.py +318 -0
- seventhings_customer_api-1.4.0/pyproject.toml +71 -0
- seventhings_customer_api-1.4.0/scripts/run-integration.sh +36 -0
- seventhings_customer_api-1.4.0/scripts/unasync.py +79 -0
- seventhings_customer_api-1.4.0/src/seventhings/__init__.py +25 -0
- seventhings_customer_api-1.4.0/src/seventhings/_async/__init__.py +0 -0
- seventhings_customer_api-1.4.0/src/seventhings/_async/client.py +583 -0
- seventhings_customer_api-1.4.0/src/seventhings/_operations.py +570 -0
- seventhings_customer_api-1.4.0/src/seventhings/_pagination.py +17 -0
- seventhings_customer_api-1.4.0/src/seventhings/_sync/__init__.py +0 -0
- seventhings_customer_api-1.4.0/src/seventhings/_sync/client.py +571 -0
- seventhings_customer_api-1.4.0/src/seventhings/_transport.py +125 -0
- seventhings_customer_api-1.4.0/src/seventhings/_version.py +1 -0
- seventhings_customer_api-1.4.0/src/seventhings/errors.py +67 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/__init__.py +175 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/_decode.py +84 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/auth.py +39 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/circularity_hub.py +90 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/enums.py +139 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/field_definitions.py +215 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/fields.py +73 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/files.py +67 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/history.py +120 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/list_options.py +216 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/persons.py +99 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/rentals.py +135 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/reports.py +29 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/tasks.py +136 -0
- seventhings_customer_api-1.4.0/src/seventhings/models/users.py +50 -0
- seventhings_customer_api-1.4.0/src/seventhings/py.typed +0 -0
- seventhings_customer_api-1.4.0/tests/__init__.py +0 -0
- seventhings_customer_api-1.4.0/tests/integration/__init__.py +0 -0
- seventhings_customer_api-1.4.0/tests/integration/conftest.py +77 -0
- seventhings_customer_api-1.4.0/tests/integration/test_live.py +851 -0
- seventhings_customer_api-1.4.0/tests/integration/test_live_new_endpoints.py +246 -0
- seventhings_customer_api-1.4.0/tests/unit/__init__.py +0 -0
- seventhings_customer_api-1.4.0/tests/unit/conftest.py +126 -0
- seventhings_customer_api-1.4.0/tests/unit/test_auth.py +124 -0
- seventhings_customer_api-1.4.0/tests/unit/test_circularity_hub.py +80 -0
- seventhings_customer_api-1.4.0/tests/unit/test_field_definitions.py +79 -0
- seventhings_customer_api-1.4.0/tests/unit/test_files.py +65 -0
- seventhings_customer_api-1.4.0/tests/unit/test_list_options.py +129 -0
- seventhings_customer_api-1.4.0/tests/unit/test_models.py +412 -0
- seventhings_customer_api-1.4.0/tests/unit/test_objects.py +102 -0
- seventhings_customer_api-1.4.0/tests/unit/test_parity.py +164 -0
- seventhings_customer_api-1.4.0/tests/unit/test_reports_pagination.py +114 -0
- seventhings_customer_api-1.4.0/tests/unit/test_rooms_locations.py +77 -0
- seventhings_customer_api-1.4.0/tests/unit/test_tasks_rentals.py +105 -0
- seventhings_customer_api-1.4.0/tests/unit/test_transport.py +179 -0
- seventhings_customer_api-1.4.0/tests/unit/test_users_persons.py +91 -0
- seventhings_customer_api-1.4.0/uv.lock +544 -0
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# Credentials for running integration tests and the examples/demo command.
|
|
2
|
+
# Copy this file to .env and fill in real values. .env is gitignored.
|
|
3
|
+
|
|
4
|
+
SEVENTHINGS_BASE_URL=https://your-instance.seventhings.com
|
|
5
|
+
SEVENTHINGS_USERNAME=user@example.com
|
|
6
|
+
SEVENTHINGS_PASSWORD=your-password
|
|
7
|
+
SEVENTHINGS_CLIENT_ID=your-client-id
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
fail-fast: false
|
|
13
|
+
matrix:
|
|
14
|
+
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"]
|
|
15
|
+
steps:
|
|
16
|
+
- uses: actions/checkout@v7
|
|
17
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
18
|
+
with:
|
|
19
|
+
python-version: ${{ matrix.python-version }}
|
|
20
|
+
- run: uv sync
|
|
21
|
+
- run: uv run ruff check .
|
|
22
|
+
- run: uv run ruff format --check .
|
|
23
|
+
- run: uv run mypy
|
|
24
|
+
- run: uv run python scripts/unasync.py --check
|
|
25
|
+
- run: uv run pytest
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
# Publishes to PyPI when a version tag (e.g. v1.4.0) is pushed. Uses PyPI
|
|
4
|
+
# trusted publishing (OIDC), so no API token is stored in the repository.
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
|
|
10
|
+
permissions:
|
|
11
|
+
contents: read
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
build:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v7
|
|
18
|
+
- uses: astral-sh/setup-uv@v10.2.0
|
|
19
|
+
with:
|
|
20
|
+
python-version: "3.14"
|
|
21
|
+
- run: uv sync
|
|
22
|
+
- run: uv run ruff check .
|
|
23
|
+
- run: uv run ruff format --check .
|
|
24
|
+
- run: uv run mypy
|
|
25
|
+
- run: uv run python scripts/unasync.py --check
|
|
26
|
+
- run: uv run pytest
|
|
27
|
+
- name: Check tag matches package version
|
|
28
|
+
run: |
|
|
29
|
+
version="$(uv run python -c 'import seventhings; print(seventhings.__version__)')"
|
|
30
|
+
if [[ "${GITHUB_REF_NAME}" != "v${version}" ]]; then
|
|
31
|
+
echo "::error::tag ${GITHUB_REF_NAME} does not match package version ${version}"
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
- run: uv build
|
|
35
|
+
- uses: actions/upload-artifact@v7
|
|
36
|
+
with:
|
|
37
|
+
name: dist
|
|
38
|
+
path: dist/
|
|
39
|
+
|
|
40
|
+
publish:
|
|
41
|
+
needs: build
|
|
42
|
+
runs-on: ubuntu-latest
|
|
43
|
+
environment:
|
|
44
|
+
name: pypi
|
|
45
|
+
url: https://pypi.org/project/seventhings-customer-api/
|
|
46
|
+
permissions:
|
|
47
|
+
id-token: write
|
|
48
|
+
steps:
|
|
49
|
+
- uses: actions/download-artifact@v8
|
|
50
|
+
with:
|
|
51
|
+
name: dist
|
|
52
|
+
path: dist/
|
|
53
|
+
- uses: pypa/gh-action-pypi-publish@v1.14.2
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
Python SDK for the seventhings Customer API. It is a port of the Go SDK (`../customer-api-go`, the reference implementation) and keeps parity with it and with the PHP SDK (`../customer-api-php`). When the SDKs disagree, Go wins.
|
|
4
|
+
|
|
5
|
+
## Commands
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
uv run pytest # unit tests (integration tests are deselected by default)
|
|
9
|
+
uv run ruff check . && uv run ruff format --check .
|
|
10
|
+
uv run mypy # strict; covers src, tests, examples, scripts
|
|
11
|
+
uv run python scripts/unasync.py # regenerate the sync client (use --check in CI)
|
|
12
|
+
scripts/run-integration.sh [pytest args] # live tests; needs .env (see .env.example)
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Layout
|
|
16
|
+
|
|
17
|
+
- `src/seventhings/_operations.py`: one builder per endpoint. Each returns an `Operation` (method, path, query, body, accept, auth flag, response parser) and does no I/O. Request shapes and decoding live **only** here.
|
|
18
|
+
- `src/seventhings/_async/client.py`: `AsyncClient` plus the `Async*Service` classes that run those operations. This is the **source** file.
|
|
19
|
+
- `src/seventhings/_sync/client.py`: **generated** by `scripts/unasync.py`. Never edit it by hand.
|
|
20
|
+
- `src/seventhings/_transport.py`: `Operation`, `Response`, URL, header and error handling.
|
|
21
|
+
- `src/seventhings/models/`: frozen dataclasses with lenient `from_dict`/`to_dict`, `str` enums, `ListOptions` query encoders, and `Fields`.
|
|
22
|
+
- `tests/unit/`: the `h` fixture (`conftest.py`) runs every test against both the sync and the async client using `httpx.MockTransport`. `test_parity.py` maps every Go method to its Python counterpart and checks that sync and async signatures match.
|
|
23
|
+
|
|
24
|
+
## Adding an endpoint
|
|
25
|
+
|
|
26
|
+
1. Add a builder to `_operations.py`.
|
|
27
|
+
2. Add the method to the matching service in `_async/client.py`.
|
|
28
|
+
3. Run `scripts/unasync.py`.
|
|
29
|
+
4. Add unit tests using the `h` fixture, and add the Go method to `GO_PARITY`.
|
|
30
|
+
|
|
31
|
+
## API quirks the code relies on (don't "fix" them)
|
|
32
|
+
|
|
33
|
+
**Created IDs**
|
|
34
|
+
- New resources return their UUID in the `Location` header.
|
|
35
|
+
- Files use `Location-UUID`; hub orders return an integer in `Location-Id`.
|
|
36
|
+
|
|
37
|
+
**Response shapes**
|
|
38
|
+
- Lists come back as `{items}` for objects, rooms, locations, files, rentals and the hub.
|
|
39
|
+
- Tasks, field definitions and report templates come back as bare arrays.
|
|
40
|
+
- Users, persons and history use full page envelopes.
|
|
41
|
+
|
|
42
|
+
**Rooms and locations**
|
|
43
|
+
- They may answer with a `{uuid, fields}` envelope; `unwrap_resource_fields` flattens it.
|
|
44
|
+
- Their `patch` returns the record, or `None` if the body is empty. Every other `patch` returns `None`.
|
|
45
|
+
|
|
46
|
+
**Persons**
|
|
47
|
+
- `create` wraps the body in `{"fields": ...}`, but `patch` does not.
|
|
48
|
+
- The UUID comes from `person_uuid` and falls back to `uuid`.
|
|
49
|
+
|
|
50
|
+
**Circularity Hub**
|
|
51
|
+
- `suggest_*` answers `[]` when there are no suggestions; the SDK returns `None`.
|
|
52
|
+
|
|
53
|
+
**Query strings**
|
|
54
|
+
- Brackets are sent literally; only values are escaped (`quote_plus`).
|
|
55
|
+
- `like`, `not_like`, `in` and `nin` use the `[]` array form.
|
|
56
|
+
|
|
57
|
+
**Headers**
|
|
58
|
+
- `Content-Type: application/json` is sent only when there is a body (archive and unarchive send none).
|
|
59
|
+
- Raw downloads and multipart uploads send no `Accept: application/json`; report creation sends `Accept: application/pdf`.
|
|
60
|
+
- Ping and `POST auth_token` never send `Authorization`.
|
|
61
|
+
|
|
62
|
+
**SSO login**
|
|
63
|
+
- Uses `grant_type=sso_auth_code` and `provider_name`. The PHP SDK's `sso`/`provider` is wrong.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 seventhings GmbH
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: seventhings-customer-api
|
|
3
|
+
Version: 1.4.0
|
|
4
|
+
Summary: Python client for the seventhings Customer API
|
|
5
|
+
Project-URL: Documentation, https://api.seventhings.com/guides/sdks/python/
|
|
6
|
+
Project-URL: Repository, https://github.com/seventhingsCompany/customer-api-python
|
|
7
|
+
Project-URL: Issues, https://github.com/seventhingsCompany/customer-api-python/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/seventhingsCompany/customer-api-python/releases
|
|
9
|
+
Author: seventhings GmbH
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,asset management,client,inventory,sdk,seventhings
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Framework :: AsyncIO
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
24
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.10
|
|
27
|
+
Requires-Dist: httpx<1,>=0.27
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# seventhings Python SDK
|
|
31
|
+
|
|
32
|
+
[](https://pypi.org/project/seventhings-customer-api/) [](https://github.com/seventhingsCompany/customer-api-python/actions/workflows/ci.yml)
|
|
33
|
+
|
|
34
|
+
Python client for the seventhings Customer API (`/customer-api/v1`). It offers the same features as the [Go](https://github.com/seventhingsCompany/customer-api-go) and [PHP](https://github.com/seventhingsCompany/customer-api-php) SDKs (v1.4.0) and comes with both a synchronous and an asynchronous client.
|
|
35
|
+
|
|
36
|
+
- Python 3.10+
|
|
37
|
+
- The only runtime dependency is [`httpx`](https://www.python-httpx.org/)
|
|
38
|
+
- Fully typed (`py.typed`), with dataclass models
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
```sh
|
|
43
|
+
pip install seventhings-customer-api
|
|
44
|
+
# or
|
|
45
|
+
uv add seventhings-customer-api
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The import name is `seventhings`.
|
|
49
|
+
|
|
50
|
+
## Quick Start
|
|
51
|
+
|
|
52
|
+
### Password authentication
|
|
53
|
+
|
|
54
|
+
```python
|
|
55
|
+
from seventhings import Client
|
|
56
|
+
|
|
57
|
+
with Client.with_credentials(
|
|
58
|
+
"https://example.seventhings.com", "user@example.com", "password", "client-id"
|
|
59
|
+
) as client:
|
|
60
|
+
print(client.objects.count())
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
### Pre-existing token
|
|
64
|
+
|
|
65
|
+
```python
|
|
66
|
+
client = Client("https://example.seventhings.com", token="my-jwt-token")
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Manual login and refresh
|
|
70
|
+
|
|
71
|
+
```python
|
|
72
|
+
client = Client("https://example.seventhings.com")
|
|
73
|
+
tok = client.auth.login("user@example.com", "password", "client-id") # stores the access token
|
|
74
|
+
tok = client.auth.refresh(tok.refresh_token) # reuses the client ID from login
|
|
75
|
+
client.auth.revoke_tokens()
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### SSO authentication
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
from seventhings.models import SSOAppTarget, SSOProviderName
|
|
82
|
+
|
|
83
|
+
tok = client.auth.login_sso(SSOProviderName.AZURE, auth_code, "client-id", SSOAppTarget.WEB)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### Async
|
|
87
|
+
|
|
88
|
+
`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`:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from seventhings import AsyncClient
|
|
92
|
+
|
|
93
|
+
async with await AsyncClient.with_credentials(url, user, password, client_id) as client:
|
|
94
|
+
obj = await client.objects.get(uuid)
|
|
95
|
+
async for room in client.rooms.all():
|
|
96
|
+
print(room.name)
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Configuration
|
|
100
|
+
|
|
101
|
+
```python
|
|
102
|
+
Client(
|
|
103
|
+
base_url, # instance URL; "/customer-api/v1" is appended
|
|
104
|
+
token=None, # bearer token
|
|
105
|
+
client_id=None, # OAuth client ID used by auth.refresh()
|
|
106
|
+
http_client=None, # your own httpx.Client / httpx.AsyncClient (not closed by the SDK)
|
|
107
|
+
timeout=30.0, # seconds; None disables the timeout
|
|
108
|
+
)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Usage
|
|
112
|
+
|
|
113
|
+
### Ping
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
ping = client.ping() # unauthenticated
|
|
117
|
+
print(ping.status) # "OK"
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Objects
|
|
121
|
+
|
|
122
|
+
Objects have a schema that differs per tenant, so they are plain `dict`s. The `all()` iterator yields `Fields`, a `dict` with typed accessors.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from seventhings.models import ListOptions, SortDirection, like
|
|
126
|
+
|
|
127
|
+
uuid = client.objects.create({"inventory_name": "Laptop", "barcode": "INV-001"})
|
|
128
|
+
obj = client.objects.get(uuid)
|
|
129
|
+
obj = client.objects.get_by_barcode("INV-001") # archived objects included
|
|
130
|
+
client.objects.patch(uuid, {"inventory_name": "Laptop (IT)"})
|
|
131
|
+
client.objects.archive(uuid)
|
|
132
|
+
client.objects.unarchive(uuid)
|
|
133
|
+
client.objects.delete(uuid)
|
|
134
|
+
|
|
135
|
+
page = client.objects.list(ListOptions(page=1, per_page=50).where(like("inventory_name", "Laptop")))
|
|
136
|
+
total = client.objects.count()
|
|
137
|
+
|
|
138
|
+
for obj in client.objects.all(ListOptions(per_page=100)): # walks every page
|
|
139
|
+
print(obj.uuid, obj.get_str("inventory_name"), obj.get_time("updated_at"))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
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`:
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from seventhings.models import FileAttachment
|
|
146
|
+
|
|
147
|
+
resp = client.objects.add_files(uuid, [FileAttachment("documents", file_uuid)])
|
|
148
|
+
client.objects.remove_files(uuid, [FileAttachment("documents", file_uuid)])
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
### History
|
|
152
|
+
|
|
153
|
+
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).
|
|
154
|
+
|
|
155
|
+
```python
|
|
156
|
+
from seventhings.models import HistoryListOptions
|
|
157
|
+
|
|
158
|
+
hist = client.persons.history(person_uuid, HistoryListOptions(page=1, per_page=20))
|
|
159
|
+
for entry in hist.items:
|
|
160
|
+
print(entry.occurred_at, entry.event_name, entry.details) # details is a JSON string
|
|
161
|
+
if hist.page * hist.per_page < hist.total:
|
|
162
|
+
... # fetch the next page
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
Object history entries are plain `dict`s, because their shape depends on `type` (`asset`, `task`, `rental_case` or `object_merge`).
|
|
166
|
+
|
|
167
|
+
### PDF reports
|
|
168
|
+
|
|
169
|
+
```python
|
|
170
|
+
from seventhings.models import CreateReport
|
|
171
|
+
|
|
172
|
+
templates = client.reports.list_templates()
|
|
173
|
+
pdf: bytes = client.reports.create(CreateReport(templates[0].uuid, [obj_uuid]))
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Files
|
|
177
|
+
|
|
178
|
+
```python
|
|
179
|
+
with open("photo.jpg", "rb") as fh:
|
|
180
|
+
file_uuid = client.files.upload("photo.jpg", fh) # or pass bytes
|
|
181
|
+
meta = client.files.get(file_uuid)
|
|
182
|
+
data = client.files.get_data(file_uuid)
|
|
183
|
+
thumb = client.files.get_thumbnail(file_uuid)
|
|
184
|
+
recent = client.files.list() # the most recent files (max 20)
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
### Tasks
|
|
188
|
+
|
|
189
|
+
```python
|
|
190
|
+
from seventhings.models import (
|
|
191
|
+
CreateTask,
|
|
192
|
+
TaskListOptions,
|
|
193
|
+
TaskReferenceInput,
|
|
194
|
+
TaskStatus,
|
|
195
|
+
TimeInterval,
|
|
196
|
+
TimeIntervalUnit,
|
|
197
|
+
UpdateTask,
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
task_uuid = client.tasks.create(
|
|
201
|
+
CreateTask(
|
|
202
|
+
title="Inspect",
|
|
203
|
+
deadline="2026-12-31",
|
|
204
|
+
assignees=[user_uuid],
|
|
205
|
+
references=[TaskReferenceInput(obj_uuid)],
|
|
206
|
+
reminders=[TimeInterval(TimeIntervalUnit.DAYS, 1)],
|
|
207
|
+
)
|
|
208
|
+
)
|
|
209
|
+
tasks = client.tasks.list(TaskListOptions(status=TaskStatus.OPEN))
|
|
210
|
+
client.tasks.update_status(task_uuid, TaskStatus.CLOSED)
|
|
211
|
+
client.tasks.update(task_uuid, UpdateTask(title="Inspect again", assignees=[user_uuid])) # PUT
|
|
212
|
+
client.tasks.delete(task_uuid)
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
### Rental cases
|
|
216
|
+
|
|
217
|
+
```python
|
|
218
|
+
from seventhings.models import (
|
|
219
|
+
CreateRentalCase,
|
|
220
|
+
RentalCaseReferenceInput,
|
|
221
|
+
RentalCaseRenter,
|
|
222
|
+
RenterType,
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
rental_uuid = client.rentals.create(
|
|
226
|
+
CreateRentalCase(
|
|
227
|
+
title="Laptop loan",
|
|
228
|
+
renter=RentalCaseRenter(RenterType.USER, user_uuid),
|
|
229
|
+
references=[RentalCaseReferenceInput(obj_uuid)],
|
|
230
|
+
issue_date="2026-01-01 09:00:00",
|
|
231
|
+
due_date="2026-01-08 09:00:00",
|
|
232
|
+
responsible_user_uuid=user_uuid,
|
|
233
|
+
)
|
|
234
|
+
)
|
|
235
|
+
rental = client.rentals.get(rental_uuid)
|
|
236
|
+
for rc in client.rentals.all():
|
|
237
|
+
print(rc.title, rc.status)
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
### Rooms and locations
|
|
241
|
+
|
|
242
|
+
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.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
loc_uuid = client.locations.create({"name": "HQ"})
|
|
246
|
+
room_uuid = client.rooms.create({"name": "Office 1", "building_id": 1})
|
|
247
|
+
room = client.rooms.patch(room_uuid, {"name": "Office 1a"})
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### Users and persons
|
|
251
|
+
|
|
252
|
+
```python
|
|
253
|
+
from seventhings.models import PersonListOptions, UserListOptions, UserSortBy, UserSortOrder
|
|
254
|
+
|
|
255
|
+
users = client.users.list(UserListOptions(sort_by=UserSortBy.EMAIL, order=UserSortOrder.ASC))
|
|
256
|
+
me = client.users.get_by_id(tok.user_id)
|
|
257
|
+
|
|
258
|
+
person_uuid = client.persons.create({"email": "ada@example.com", "first_name": "Ada"})
|
|
259
|
+
person = client.persons.get(person_uuid)
|
|
260
|
+
print(person.email, person.fields.get_str("cost_center")) # every field is in person.fields
|
|
261
|
+
client.persons.patch(person_uuid, {"department": "IT"})
|
|
262
|
+
for p in client.persons.all(PersonListOptions(sort_by="last_name")):
|
|
263
|
+
...
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
### Field definitions
|
|
267
|
+
|
|
268
|
+
```python
|
|
269
|
+
from seventhings.models import AssetTrackingTemplate
|
|
270
|
+
|
|
271
|
+
defs = client.field_definitions.list(AssetTrackingTemplate.ASSET)
|
|
272
|
+
required = client.field_definitions.mandatory(AssetTrackingTemplate.ASSET)
|
|
273
|
+
missing = client.field_definitions.missing_mandatory_fields(AssetTrackingTemplate.ASSET, payload)
|
|
274
|
+
options = defs[0].field_type.allowed_values() # dropdown values
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
### Circularity Hub
|
|
278
|
+
|
|
279
|
+
Items and orders in the Circularity Hub use integer IDs.
|
|
280
|
+
|
|
281
|
+
```python
|
|
282
|
+
from seventhings.models import AddObjectEntry, FilterObject, FilterOperator
|
|
283
|
+
|
|
284
|
+
suggestions = client.circularity_hub.suggest_category(
|
|
285
|
+
FilterObject(filter={"uuid": {FilterOperator.IN: [obj_uuid]}})
|
|
286
|
+
) # None when there are no suggestions
|
|
287
|
+
client.circularity_hub.add_objects({obj_uuid: AddObjectEntry("chairs", "25.00")})
|
|
288
|
+
for item in client.circularity_hub.all_items():
|
|
289
|
+
...
|
|
290
|
+
order_id = client.circularity_hub.create_order([1, 2])
|
|
291
|
+
order = client.circularity_hub.get_order(order_id)
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Raw requests
|
|
295
|
+
|
|
296
|
+
`request()` covers anything the SDK doesn't wrap:
|
|
297
|
+
|
|
298
|
+
```python
|
|
299
|
+
resp = client.request("GET", "objects", query="page=1&per_page=5")
|
|
300
|
+
print(resp.status_code, resp.json())
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
## Filtering and sorting
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
from seventhings.models import ListOptions, SortDirection, gte, in_, like
|
|
307
|
+
|
|
308
|
+
opts = (
|
|
309
|
+
ListOptions(page=1, per_page=50)
|
|
310
|
+
.sort_by("name", SortDirection.ASC)
|
|
311
|
+
.sort_by("created_at", SortDirection.DESC)
|
|
312
|
+
.where(like("name", "Laptop"), in_("status", "active", "pending"), gte("price", "100"))
|
|
313
|
+
)
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
This produces the following query string (brackets are sent literally and only the values are escaped):
|
|
317
|
+
|
|
318
|
+
```
|
|
319
|
+
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
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
| Operator | Helper | Description |
|
|
323
|
+
|----------|--------|-------------|
|
|
324
|
+
| `eq` | `eq` | Equal |
|
|
325
|
+
| `neq` | `neq` | Not equal |
|
|
326
|
+
| `gt`, `gte` | `gt`, `gte` | Greater than (or equal) |
|
|
327
|
+
| `gt_or_null`, `gte_or_null` | `gt_or_null`, `gte_or_null` | Greater than (or equal), including null |
|
|
328
|
+
| `lt`, `lte` | `lt`, `lte` | Less than (or equal) |
|
|
329
|
+
| `lt_or_null`, `lte_or_null` | `lt_or_null`, `lte_or_null` | Less than (or equal), including null |
|
|
330
|
+
| `like` | `like` | Contains substring (multi-value) |
|
|
331
|
+
| `not_like` | `not_like` | Does not contain substring (multi-value) |
|
|
332
|
+
| `in` | `in_` | Value in set (multi-value) |
|
|
333
|
+
| `nin` | `nin` | Value not in set (multi-value) |
|
|
334
|
+
|
|
335
|
+
## Error handling
|
|
336
|
+
|
|
337
|
+
Every exception derives from `seventhings.SeventhingsError`:
|
|
338
|
+
|
|
339
|
+
| Exception | When |
|
|
340
|
+
|-----------|------|
|
|
341
|
+
| `APIError` | The API returned a status of 400 or higher |
|
|
342
|
+
| `NetworkError` | Connection failure, timeout or another transport error (wraps the `httpx` exception) |
|
|
343
|
+
| `DecodeError` | The response could not be decoded, e.g. invalid JSON or a missing `Location` header |
|
|
344
|
+
|
|
345
|
+
```python
|
|
346
|
+
from seventhings import APIError
|
|
347
|
+
|
|
348
|
+
try:
|
|
349
|
+
client.objects.get("nonexistent")
|
|
350
|
+
except APIError as err:
|
|
351
|
+
print(err.status_code, err.body)
|
|
352
|
+
if err.is_not_found: # also: is_unauthorized, is_forbidden, is_conflict,
|
|
353
|
+
... # is_rate_limited, is_server_error, is_feature_inactive
|
|
354
|
+
```
|
|
355
|
+
|
|
356
|
+
`is_feature_inactive` is true when the endpoint's module (for example rentals) is not active on the instance.
|
|
357
|
+
|
|
358
|
+
## Pagination
|
|
359
|
+
|
|
360
|
+
`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:
|
|
361
|
+
- they ignore `opts.page`;
|
|
362
|
+
- they use `opts.per_page` as the page size, defaulting to 100;
|
|
363
|
+
- they stop at the first page that is shorter than the page size.
|
|
364
|
+
|
|
365
|
+
Tasks and files have no paging. History is paged manually.
|
|
366
|
+
|
|
367
|
+
## Scope and limitations
|
|
368
|
+
|
|
369
|
+
- **No automatic token refresh.** Call `client.auth.refresh(refresh_token)` yourself when the access token expires.
|
|
370
|
+
- **No automatic retry and no rate limiting.** Wrap calls yourself if you need either, or pass an `httpx` client that uses a retrying transport.
|
|
371
|
+
- **Dates are strings**, as the API sends them (`Y-m-d H:i:s`, UTC). `Fields.get_time()` parses them.
|
|
372
|
+
- **Unknown enum values** from newer API versions are kept as plain strings rather than raising an error.
|
|
373
|
+
|
|
374
|
+
## Development
|
|
375
|
+
|
|
376
|
+
```sh
|
|
377
|
+
uv sync
|
|
378
|
+
uv run pytest # unit tests; each one runs against both Client and AsyncClient
|
|
379
|
+
uv run ruff check . && uv run ruff format --check .
|
|
380
|
+
uv run mypy # strict
|
|
381
|
+
uv run python scripts/unasync.py # regenerate the sync client after editing _async/client.py
|
|
382
|
+
```
|
|
383
|
+
|
|
384
|
+
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.
|
|
385
|
+
|
|
386
|
+
### Integration tests
|
|
387
|
+
|
|
388
|
+
The integration tests run against a live instance and create, then delete, their own test data:
|
|
389
|
+
|
|
390
|
+
```sh
|
|
391
|
+
cp .env.example .env # fill in SEVENTHINGS_BASE_URL/USERNAME/PASSWORD/CLIENT_ID
|
|
392
|
+
scripts/run-integration.sh # or: uv run pytest -m integration
|
|
393
|
+
scripts/run-integration.sh -k person # forward pytest args
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
[`examples/demo/`](https://github.com/seventhingsCompany/customer-api-python/tree/main/examples/demo) walks through the SDK end to end and uses the same environment variables.
|
|
397
|
+
|
|
398
|
+
## License
|
|
399
|
+
|
|
400
|
+
MIT
|