This page explains how the desktop client reaches the server API. It covers core/api_client.py (ApiClient and its error classes), core/api_worker.py (ApiWorker, ApiMixin, format_api_error), core/generic_api.py (GenericApi), and core/core_api.py (notes, attachments, and e-mail wrappers), all under src/cluster_erp/desktop/. The change signals that mutations emit are described on signal bus and watch_entity.
In short¶
- One
ApiClientwraps onehttpx.Clientwith a 30-second timeout, a base address, and anAccept-Languageheader (src/cluster_erp/desktop/core/api_client.py:80-96). - Screens run calls in
ApiWorker, aQThread, throughApiMixin.call_api. Results and errors come back as Qt signals (src/cluster_erp/desktop/core/api_worker.py:28-65,src/cluster_erp/desktop/core/api_worker.py:187-259). - A mutation called with an
entitydescriptor emits a change signal after the server answers (src/cluster_erp/desktop/core/api_client.py:223-294). - HTTP errors become typed exceptions, and
format_api_errorturns them into a message in the user's language (src/cluster_erp/desktop/core/api_client.py:55-77,src/cluster_erp/desktop/core/api_worker.py:108-176). GenericApibuilds every entity path from the endpoint declarations in the metadata (src/cluster_erp/desktop/core/generic_api.py:24-31).
How it works¶
The API client¶
ApiClient.__init__ creates the httpx.Client with the base address, a 30-second timeout, redirects followed, and Accept-Language set to en (src/cluster_erp/desktop/core/api_client.py:81-91). main() then sets the language of the translator, and every language change updates the header (src/cluster_erp/desktop/main.py:169-172, src/cluster_erp/desktop/core/api_client.py:110-112). set_base_url replaces the httpx.Client and keeps its headers (src/cluster_erp/desktop/core/api_client.py:98-108).
login posts the user name and the password to /auth/login. On success it keeps the access token, the refresh token, the user, and the permission list in memory and attaches the access token to every later request (src/cluster_erp/desktop/core/api_client.py:134-163). The tokens are not written to the settings store: the only settings keys the client writes are listed on languages, themes, and accessibility. logout clears all four in memory (src/cluster_erp/desktop/core/api_client.py:165-171). Token handling on the server is described on authentication and sessions.
_request sends one request. When the answer is 401, it calls _try_refresh_token once and repeats the request once if the refresh worked (src/cluster_erp/desktop/core/api_client.py:335-349). A failed refresh clears the tokens and emits session_expired on the signal bus (src/cluster_erp/desktop/core/api_client.py:173-211). A 204 answer returns None; any other success returns the parsed JSON (src/cluster_erp/desktop/core/api_client.py:350-353). download follows the same refresh rule and returns the bytes, the content type, and the file name (src/cluster_erp/desktop/core/api_client.py:296-324).
The lock in ApiClient is held only to read the current httpx.Client and around a token refresh, not for the whole round trip. The comment states that a slow background request would otherwise freeze calls from the GUI thread (src/cluster_erp/desktop/core/api_client.py:336-341).
Error classes¶
_parse_error_response maps the status code to an exception class and reads the server's error body. A body with an error object fills message, code, trace_id, and details; another body fills only the message from detail (src/cluster_erp/desktop/core/api_client.py:64-77). A transport failure raises ApiError with status 0 and code CONNECTION_ERROR (src/cluster_erp/desktop/core/api_client.py:359-364).
| Status | Exception | Source |
|---|---|---|
| 401 | AuthenticationError |
src/cluster_erp/desktop/core/api_client.py:56 |
| 403 | ForbiddenError |
src/cluster_erp/desktop/core/api_client.py:57 |
| 404 | NotFoundError |
src/cluster_erp/desktop/core/api_client.py:58 |
| 409 | ConflictError |
src/cluster_erp/desktop/core/api_client.py:59 |
| 422 | ValidationError |
src/cluster_erp/desktop/core/api_client.py:60 |
| any other error status | ServerError |
src/cluster_erp/desktop/core/api_client.py:65 |
| no HTTP answer | ApiError with code CONNECTION_ERROR |
src/cluster_erp/desktop/core/api_client.py:359-364 |
The server's error codes are listed on errors and exceptions.
Worker thread model¶
The diagram shows one call from a screen that mixes in ApiMixin.
sequenceDiagram participant V as Screen with ApiMixin participant W as ApiWorker QThread participant A as ApiClient participant S as Server V->>W: call_api(func, args, on_success, on_error) V->>W: start W->>A: func(args) A->>S: HTTP request S-->>A: response A-->>W: result or ApiError W-->>V: finished(result) or error(msg, status, code, details, trace_id) V->>V: liveness check, then on_success or on_error or toast V->>V: remove worker from the list
In words:
call_apicreates anApiWorkerwith the function and its arguments, connects the callbacks, adds the worker to the screen's list, and starts it (src/cluster_erp/desktop/core/api_worker.py:187-259).ApiWorker.runcalls the function on the worker thread. It emitsfinishedwith the result, orerrorwith the message, the status, the code, the details, and the trace id (src/cluster_erp/desktop/core/api_worker.py:43-65).- Every callback first checks that the screen's Qt object still exists. The comment states that a closed tab would otherwise raise an error for a deleted object (
src/cluster_erp/desktop/core/api_worker.py:197-209). - On an error,
format_api_errorbuilds a localised message. If the screen gave noon_error, the message appears as an error toast (src/cluster_erp/desktop/core/api_worker.py:219-249). - When the screen is destroyed,
_stop_all_workerswaits up to 5,000 ms for each running worker and never terminates one (src/cluster_erp/desktop/core/api_worker.py:25,src/cluster_erp/desktop/core/api_worker.py:265-289).
Thirty-three classes under src/cluster_erp/desktop/ mix in ApiMixin, counted by searching for class definitions that name ApiMixin. Two places create ApiWorker directly: the sign-in dialog and the status bar (src/cluster_erp/desktop/widgets/login_dialog.py:306, src/cluster_erp/desktop/widgets/status_bar.py:57).
Localised error messages¶
format_api_error chooses the message in this order (src/cluster_erp/desktop/core/api_worker.py:108-176):
- For
VALIDATION_ERRORwith afieldslist, one line per field, with the field label fromfield.<name>and the reason mapped through_REASON_KEYS(src/cluster_erp/desktop/core/api_worker.py:85-105,src/cluster_erp/desktop/core/api_worker.py:123-132). - For a
details.missinglist, one line per missing item, with the label in the current language (src/cluster_erp/desktop/core/api_worker.py:134-146). - The translation of
error.<code>, filled with the scalar details. Aresourcedetail is translated throughresource.<name>(src/cluster_erp/desktop/core/api_worker.py:148-165). - The translation of
error.unknown(src/cluster_erp/desktop/core/api_worker.py:167-168).
A trace id, when present, is appended on its own line (src/cluster_erp/desktop/core/api_worker.py:170-174).
The GenericApi class¶
GenericApi(client, module_name, entity_meta) prefixes every path with /<module> and replaces {id}, {parent_id}, and {scope_id} placeholders. An endpoint key that the metadata does not declare raises ValueError (src/cluster_erp/desktop/core/generic_api.py:10-31).
| Method | HTTP call | Change signal | Source |
|---|---|---|---|
list |
GET of the chosen list endpoint with search, limit and offset when the list view is paginated, the parent and scope filters, and extra filters |
none | src/cluster_erp/desktop/core/generic_api.py:33-61 |
get |
GET of the get endpoint |
none | src/cluster_erp/desktop/core/generic_api.py:63-65 |
create |
POST of the create endpoint |
entity_created |
src/cluster_erp/desktop/core/generic_api.py:67-73 |
update |
PATCH when the update endpoint declares it, otherwise PUT |
entity_updated |
src/cluster_erp/desktop/core/generic_api.py:75-84 |
delete |
DELETE of the delete endpoint |
entity_deleted |
src/cluster_erp/desktop/core/generic_api.py:86-88 |
bulk_delete |
POST of the bulk_delete endpoint with {ids} |
entity_created |
src/cluster_erp/desktop/core/generic_api.py:92-99 |
execute_action |
POST or GET of an action endpoint |
entity_created for POST |
src/cluster_erp/desktop/core/generic_api.py:101-122 |
A server-side delete is a deactivation; see soft delete and deactivation. GenericApi.delete sends no deactivation reason.
Shared wrappers¶
core_api.py holds three wrappers that screens use for cross-cutting features.
| Class | Calls | Source |
|---|---|---|
NotesApi |
/notes/{entity_type}/{entity_id}, /notes/count/..., POST /notes/, PUT and DELETE /notes/{id}, POST /notes/bulk-delete |
src/cluster_erp/desktop/core/core_api.py:9-42 |
AttachmentsApi |
list, count, multipart upload, download, metadata update, file replace, delete, bulk delete under /attachments/ |
src/cluster_erp/desktop/core/core_api.py:45-107 |
EmailApi |
POST /email/resolve, POST /email/send-records, POST /email/send |
src/cluster_erp/desktop/core/core_api.py:110-174 |
The server side of notes and attachments is on attachments, notes, and storage. The e-mail routes are on PDF and system mail.
Why it is built this way¶
- Worker threads instead of an event loop: the project's written engineering rules state that the client uses an
httpxsynchronous client withQThreadworkers and avoids an async event loop to avoid complexity. - No forced worker termination: the comment in
src/cluster_erp/desktop/core/api_worker.py:277-283states that terminating a blocked worker would leave the client lock held and block every later call. - The update verb from metadata: the comment in
src/cluster_erp/desktop/core/generic_api.py:77-78states that the web client reads the same declaration.
Rules and boundaries¶
- Paths for metadata-driven entities come from the metadata.
GenericApi._pathrefuses an undeclared endpoint key (src/cluster_erp/desktop/core/generic_api.py:24-28). - Permission checks in the client are display aids.
has_permissionanswers from the list returned at sign-in (src/cluster_erp/desktop/core/api_client.py:131-132); the server enforces every permission, see roles and permissions.
Interaction with other parts¶
- Every screen built from metadata uses
GenericApi; see generic widgets. FkCachecallsApiClient.getdirectly to load option lists; see metadata store and foreign-key cache.- Hand-written screens call
ApiClientdirectly, most of them throughcall_api; see module screens.
Limits and open points¶
- Some calls run on the GUI thread without a worker: the metadata load at start-up (
src/cluster_erp/desktop/main.py:196-200), foreign-key option loads (src/cluster_erp/desktop/widgets/generic/generic_form.py:328-343), and the chat widget's list and count queries (src/cluster_erp/desktop/widgets/chat/chat_widget.py:1651,src/cluster_erp/desktop/widgets/chat/chat_widget.py:1758). bulk_deleteemitsentity_deletedonce per id after the call, andexecute_actionwithPOSTemitsentity_updatedfor a record action orentity_createdfor an action without a target; both callemit_entity_changethemselves (src/cluster_erp/desktop/core/generic_api.py:92-99,src/cluster_erp/desktop/core/generic_api.py:101-121).execute_actionsendsGETfor every method other thanPOST(src/cluster_erp/desktop/core/generic_api.py:113-122). List screens use their own action helper that also handlesPUT(src/cluster_erp/desktop/widgets/generic/generic_list_view.py:774-787).- The comment in
src/cluster_erp/desktop/core/api_worker.py:277-280says a blocked worker holds the client lock for the whole request._requestholds the lock only around reading the client and around a refresh (src/cluster_erp/desktop/core/api_client.py:336-347).
Terms introduced¶
| Term | Code identifier | Definition | Turkish term | Source |
|---|---|---|---|---|
| API client | ApiClient |
The desktop object that sends every HTTP request, holds the session in memory, and emits change signals. | API istemcisi | src/cluster_erp/desktop/core/api_client.py:80 |
| API worker | ApiWorker |
A QThread that runs one API call and reports the result or the error as a signal. |
API çalışanı | src/cluster_erp/desktop/core/api_worker.py:28 |
| API mixin | ApiMixin |
The mixin that gives a widget call_api and stops its workers when the widget is destroyed. |
API karışımı | src/cluster_erp/desktop/core/api_worker.py:179 |
| entity descriptor | EntityDescriptor |
A frozen pair of module name and entity type that names the subject of a change signal. | varlık tanımlayıcı | src/cluster_erp/desktop/core/entity_types.py:7 |
| generic API (desktop) | GenericApi |
The desktop class that builds entity calls from metadata endpoint declarations. | genel API | src/cluster_erp/desktop/core/generic_api.py:10 |
Related pages¶
- Desktop client
- Signal bus and watch_entity
- Generic widgets
- Boot, session, and API calls
- Errors and exceptions
Verified against commit 92f8051 on 2026-11-13.