Factorizer/Cluster ERP WikiCluster ERP Türkçe
Home / Desktop client

Explanation

API client and workers

How the desktop client calls the server through ApiClient, runs calls in QThread workers with ApiMixin, maps errors, and builds entity calls with GenericApi.

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 ApiClient wraps one httpx.Client with a 30-second timeout, a base address, and an Accept-Language header (src/cluster_erp/desktop/core/api_client.py:80-96).
  • Screens run calls in ApiWorker, a QThread, through ApiMixin.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 entity descriptor 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_error turns 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).
  • GenericApi builds 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:

  1. call_api creates an ApiWorker with 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).
  2. ApiWorker.run calls the function on the worker thread. It emits finished with the result, or error with the message, the status, the code, the details, and the trace id (src/cluster_erp/desktop/core/api_worker.py:43-65).
  3. 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).
  4. On an error, format_api_error builds a localised message. If the screen gave no on_error, the message appears as an error toast (src/cluster_erp/desktop/core/api_worker.py:219-249).
  5. When the screen is destroyed, _stop_all_workers waits 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):

  1. For VALIDATION_ERROR with a fields list, one line per field, with the field label from field.<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).
  2. For a details.missing list, one line per missing item, with the label in the current language (src/cluster_erp/desktop/core/api_worker.py:134-146).
  3. The translation of error.<code>, filled with the scalar details. A resource detail is translated through resource.<name> (src/cluster_erp/desktop/core/api_worker.py:148-165).
  4. 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 httpx synchronous client with QThread workers 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-283 states 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-78 states that the web client reads the same declaration.

Rules and boundaries

  • Paths for metadata-driven entities come from the metadata. GenericApi._path refuses an undeclared endpoint key (src/cluster_erp/desktop/core/generic_api.py:24-28).
  • Permission checks in the client are display aids. has_permission answers 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

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_delete emits entity_deleted once per id after the call, and execute_action with POST emits entity_updated for a record action or entity_created for an action without a target; both call emit_entity_change themselves (src/cluster_erp/desktop/core/generic_api.py:92-99, src/cluster_erp/desktop/core/generic_api.py:101-121).
  • execute_action sends GET for every method other than POST (src/cluster_erp/desktop/core/generic_api.py:113-122). List screens use their own action helper that also handles PUT (src/cluster_erp/desktop/widgets/generic/generic_list_view.py:774-787).
  • The comment in src/cluster_erp/desktop/core/api_worker.py:277-280 says a blocked worker holds the client lock for the whole request. _request holds 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

Verified against commit 92f8051 on 2026-11-13.

Verified against commit 92f8051 · 2026-11-13