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.
Files changed (60) hide show
  1. seventhings_customer_api-1.4.0/.env.example +7 -0
  2. seventhings_customer_api-1.4.0/.github/workflows/ci.yml +25 -0
  3. seventhings_customer_api-1.4.0/.github/workflows/release.yml +53 -0
  4. seventhings_customer_api-1.4.0/.gitignore +13 -0
  5. seventhings_customer_api-1.4.0/CLAUDE.md +63 -0
  6. seventhings_customer_api-1.4.0/LICENSE +21 -0
  7. seventhings_customer_api-1.4.0/PKG-INFO +400 -0
  8. seventhings_customer_api-1.4.0/README.md +371 -0
  9. seventhings_customer_api-1.4.0/examples/demo/README.md +174 -0
  10. seventhings_customer_api-1.4.0/examples/demo/demo.py +318 -0
  11. seventhings_customer_api-1.4.0/pyproject.toml +71 -0
  12. seventhings_customer_api-1.4.0/scripts/run-integration.sh +36 -0
  13. seventhings_customer_api-1.4.0/scripts/unasync.py +79 -0
  14. seventhings_customer_api-1.4.0/src/seventhings/__init__.py +25 -0
  15. seventhings_customer_api-1.4.0/src/seventhings/_async/__init__.py +0 -0
  16. seventhings_customer_api-1.4.0/src/seventhings/_async/client.py +583 -0
  17. seventhings_customer_api-1.4.0/src/seventhings/_operations.py +570 -0
  18. seventhings_customer_api-1.4.0/src/seventhings/_pagination.py +17 -0
  19. seventhings_customer_api-1.4.0/src/seventhings/_sync/__init__.py +0 -0
  20. seventhings_customer_api-1.4.0/src/seventhings/_sync/client.py +571 -0
  21. seventhings_customer_api-1.4.0/src/seventhings/_transport.py +125 -0
  22. seventhings_customer_api-1.4.0/src/seventhings/_version.py +1 -0
  23. seventhings_customer_api-1.4.0/src/seventhings/errors.py +67 -0
  24. seventhings_customer_api-1.4.0/src/seventhings/models/__init__.py +175 -0
  25. seventhings_customer_api-1.4.0/src/seventhings/models/_decode.py +84 -0
  26. seventhings_customer_api-1.4.0/src/seventhings/models/auth.py +39 -0
  27. seventhings_customer_api-1.4.0/src/seventhings/models/circularity_hub.py +90 -0
  28. seventhings_customer_api-1.4.0/src/seventhings/models/enums.py +139 -0
  29. seventhings_customer_api-1.4.0/src/seventhings/models/field_definitions.py +215 -0
  30. seventhings_customer_api-1.4.0/src/seventhings/models/fields.py +73 -0
  31. seventhings_customer_api-1.4.0/src/seventhings/models/files.py +67 -0
  32. seventhings_customer_api-1.4.0/src/seventhings/models/history.py +120 -0
  33. seventhings_customer_api-1.4.0/src/seventhings/models/list_options.py +216 -0
  34. seventhings_customer_api-1.4.0/src/seventhings/models/persons.py +99 -0
  35. seventhings_customer_api-1.4.0/src/seventhings/models/rentals.py +135 -0
  36. seventhings_customer_api-1.4.0/src/seventhings/models/reports.py +29 -0
  37. seventhings_customer_api-1.4.0/src/seventhings/models/tasks.py +136 -0
  38. seventhings_customer_api-1.4.0/src/seventhings/models/users.py +50 -0
  39. seventhings_customer_api-1.4.0/src/seventhings/py.typed +0 -0
  40. seventhings_customer_api-1.4.0/tests/__init__.py +0 -0
  41. seventhings_customer_api-1.4.0/tests/integration/__init__.py +0 -0
  42. seventhings_customer_api-1.4.0/tests/integration/conftest.py +77 -0
  43. seventhings_customer_api-1.4.0/tests/integration/test_live.py +851 -0
  44. seventhings_customer_api-1.4.0/tests/integration/test_live_new_endpoints.py +246 -0
  45. seventhings_customer_api-1.4.0/tests/unit/__init__.py +0 -0
  46. seventhings_customer_api-1.4.0/tests/unit/conftest.py +126 -0
  47. seventhings_customer_api-1.4.0/tests/unit/test_auth.py +124 -0
  48. seventhings_customer_api-1.4.0/tests/unit/test_circularity_hub.py +80 -0
  49. seventhings_customer_api-1.4.0/tests/unit/test_field_definitions.py +79 -0
  50. seventhings_customer_api-1.4.0/tests/unit/test_files.py +65 -0
  51. seventhings_customer_api-1.4.0/tests/unit/test_list_options.py +129 -0
  52. seventhings_customer_api-1.4.0/tests/unit/test_models.py +412 -0
  53. seventhings_customer_api-1.4.0/tests/unit/test_objects.py +102 -0
  54. seventhings_customer_api-1.4.0/tests/unit/test_parity.py +164 -0
  55. seventhings_customer_api-1.4.0/tests/unit/test_reports_pagination.py +114 -0
  56. seventhings_customer_api-1.4.0/tests/unit/test_rooms_locations.py +77 -0
  57. seventhings_customer_api-1.4.0/tests/unit/test_tasks_rentals.py +105 -0
  58. seventhings_customer_api-1.4.0/tests/unit/test_transport.py +179 -0
  59. seventhings_customer_api-1.4.0/tests/unit/test_users_persons.py +91 -0
  60. 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,13 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ dist/
5
+ build/
6
+ *.egg-info/
7
+ .pytest_cache/
8
+ .mypy_cache/
9
+ .ruff_cache/
10
+ .DS_Store
11
+
12
+ # Local credentials for integration tests / examples/demo/demo.py.
13
+ .env
@@ -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
+ [![PyPI](https://img.shields.io/pypi/v/seventhings-customer-api)](https://pypi.org/project/seventhings-customer-api/) [![CI](https://github.com/seventhingsCompany/customer-api-python/actions/workflows/ci.yml/badge.svg)](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