Skip to content

Users API

The UsersAPI client exposes the user read endpoints (system user list, own profile, edit form) and, since the admin write group opened by ADR 0001, the four admin write endpoints: create, edit, delete and edit credentials.

Usage

from pynteracta.client import InteractaClient

with InteractaClient(base_url="https://tenant.example.com", credentials=...) as client:
    page = client.users.list(page_size=50)
    for u in page.items_typed:
        print(u.id, u.firstName, u.lastName, u.contactEmail)

    me = client.users.profile()
    print(me.first_name, me.contact_email)

    # Edit form (admin): the current state plus the optimistic-concurrency token
    user = client.users.get_for_edit(1042)
    print(user.first_name, user.last_name, user.contact_email, user.blocked)
    print(user.occ_token)  # the token to pass to edit()

UserForEdit.blocked is read-only: the edit DTO has no blocked field, so blocking and unblocking a user is not something this library can do.

Writing users

create(), edit(), delete() and edit_credentials() mirror the other write groups: explicit snake_case keyword arguments, a *_raw() variant for callers holding a pre-built DTO, and a facade over the server response. Only the fields you pass are sent, in one request; nested blocks (user_preferences, user_info, user_settings, user_credentials_configuration, reset_user_custom_credentials_command) are passed as dicts in the shape of the DTO (camelCase keys) or as generated DTOs.

from pynteracta.client import InteractaClient
from pynteracta.exceptions import ConcurrencyError

with InteractaClient(base_url="https://tenant.example.com", credentials=...) as client:
    created = client.users.create(
        firstname="Maria",
        lastname="Rossi",
        contact_email="m.rossi@tenant.example.com",
        user_credentials_configuration={
            "custom": {"username": "m.rossi@tenant.example.com", "active": True}
        },
        reset_user_custom_credentials_command={"generatePassword": True},
    )
    print(created.user_id, created.next_occ_token)
    print(created.generated_password)  # ["..."]: hand it over, it is shown only here

    # Edit: read first, pass the token, and send back every field that must survive
    user = client.users.get_for_edit(created.user_id)
    try:
        edited = client.users.edit(
            created.user_id,
            user.occ_token,
            firstname=user.first_name,
            lastname="Rossi-Bianchi",
            contact_email=user.contact_email,
        )
    except ConcurrencyError:
        raise  # the user changed after we read it: re-read and decide, the library never retries
    print(edited.next_occ_token)

    # Credentials: only the blocks you pass are touched
    creds = client.users.get_credentials_for_edit(created.user_id)
    client.users.edit_credentials(
        created.user_id,
        creds.occ_token,
        google={"googleAccountId": "m.rossi@tenant.example.com", "enabled": True},
    )

    client.users.delete(created.user_id)

UserWriteResult

create(), edit() and edit_credentials() return a UserWriteResult: user_id (from the create response, otherwise the id you passed), next_occ_token (the token for the next edit), generated_password, expired_credentials, sent_email_notify (create only), account_photo_url, and .raw (the generated response DTO). delete() returns nothing: the response has no body.

Concurrency: occ_token

edit() and edit_credentials() take the token read with get_for_edit() (UserForEdit.occ_token) and get_credentials_for_edit() (UserCredentialsForEdit.occ_token). If the user changed in the meantime the server answers 409, mapped to ConcurrencyError. The library does not re-read and retry (ADR 0001): one request per call, also on timeouts and network errors, whose outcome stays unknown until you check.

Generated password

Ask the server to generate the custom password with reset_user_custom_credentials_command={"generatePassword": True}, or set one with {"password": ["..."]}; forceCredentialsExpiration forces a change at first login and emailNotifyRecipients sends it by email. The password can only be set at creation: the credentials edit DTO has no password field. The swagger declares generatedPassword as a list of strings while the tenant returns a single string: generated_password always gives you a list. Request and response bodies are redacted before they reach logs, audit log and hooks (password, generatedPassword → ***REDACTED***), and a malformed create response raises a ValueError that names the fields, never the values.

Omitted fields in edit(): the server replaces the user

Verified against a tenant (spec 05): edit() requires firstname and lastname (400 REQUIRED_FIELD otherwise) and treats the request as a replacement of the scalar fields: contact_email, private_email and external_id that are missing from the request are cleared. The nested blocks are different: user_preferences, user_info and user_settings you omit are kept.

Pass every scalar field you want to keep

Read the user first and send back first name, last name, contact email, private email and external id. The CLI users edit does this for you.

Credential blocks in edit_credentials(): omitted blocks are kept

edit_credentials() takes up to three blocks, google, microsoft and custom. A block you do not pass is kept as it is (a call without any block changes nothing). The server has no notion of a disabled block: to remove one, send it with the flag false and no account id (custom={"active": False}, google={"enabled": False}); an account id or username together with the flag false is rejected (400 INVALID_VALUE). custom.active is required whenever the custom block is present (400 REQUIRED_FIELD), and usernames and account ids must belong to the tenant's domain (400 INVALID_DOMAIN).

Deleting a user

After delete() the user is gone: the credentials form answers 404 (NotFoundError) and the edit form answers 204 with an empty body, which get_for_edit() maps to NotFoundError as well (status_code == 204). The tenant gives that 204 for any unknown user id.

API Reference

pynteracta.api.users.UsersAPI

Bases: ResourceClient

Client for user listing, profile, admin lookup and admin write endpoints.

Le scritture (spec 05) inviano solo i campi passati, in una sola richiesta: un errore del server o di rete risale al chiamante, mai un nuovo tentativo (ADR 0001). Il token di concorrenza lo fornisce il chiamante, letto da UserForEdit.occ_token o UserCredentialsForEdit.occ_token; un 409 è ConcurrencyError. La semantica del server sui campi omessi in edit ed edit_credentials è documentata in docs/api/users.md.

create(*, firstname=None, lastname=None, contact_email=None, private_email=None, external_id=None, user_preferences=None, user_info=None, user_settings=None, user_credentials_configuration=None, reset_user_custom_credentials_command=None)

POST /admin/manage/users: crea un utente.

Il corpo contiene solo i campi passati, in camelCase. I blocchi annidati sono dict nella forma del DTO o DTO generati. La password custom si imposta o si fa generare solo qui, con reset_user_custom_credentials_command (generatePassword oppure password).

Parameters:

Name Type Description Default
firstname str | None

Nome.

None
lastname str | None

Cognome.

None
contact_email str | None

Email di contatto.

None
private_email str | None

Email privata.

None
external_id str | None

Riferimento esterno.

None
user_preferences WriteBlock

AdminUserPreferencesDTO (lingua, fuso, notifiche email).

None
user_info WriteBlock

UserInfoDTO (area, business unit, email privata).

None
user_settings WriteBlock

UserSettingsRequestDTO (sezione persone, profilo ridotto…).

None
user_credentials_configuration WriteBlock

UserCredentialsConfigurationDTO (google, microsoft, custom).

None
reset_user_custom_credentials_command WriteBlock

ResetUserCustomCredentialsCommandDTO.

None

create_raw(req)

POST /admin/manage/users da un DTO già costruito.

delete(user_id)

DELETE /admin/manage/users/{userId}: elimina un utente. La risposta non ha corpo.

edit(user_id, occ_token, *, firstname=None, lastname=None, contact_email=None, private_email=None, external_id=None, user_preferences=None, user_info=None, user_settings=None)

PUT /admin/manage/users/{userId}: modifica un utente.

Il corpo contiene solo i campi passati più l'occToken. I campi omessi non vengono inviati: cosa ne fa il server è scritto in docs/api/users.md.

Parameters:

Name Type Description Default
user_id int

L'utente da modificare.

required
occ_token int

Token di concorrenza letto da UserForEdit.occ_token.

required
firstname str | None

Nome.

None
lastname str | None

Cognome.

None
contact_email str | None

Email di contatto.

None
private_email str | None

Email privata.

None
external_id str | None

Riferimento esterno.

None
user_preferences WriteBlock

AdminUserPreferencesDTO.

None
user_info WriteBlock

UserInfoDTO.

None
user_settings WriteBlock

UserSettingsRequestDTO.

None

edit_credentials(user_id, occ_token, *, google=None, microsoft=None, custom=None)

PUT /admin/manage/users/{userId}/credentials: modifica le credenziali.

userCredentialsConfiguration contiene solo i blocchi passati. La password custom non si cambia da qui: il DTO non la prevede (solo alla creazione).

Parameters:

Name Type Description Default
user_id int

L'utente.

required
occ_token int

Token letto da UserCredentialsForEdit.occ_token.

required
google WriteBlock

GoogleUserCredentialsConfigurationDTO (googleAccountId, enabled).

None
microsoft WriteBlock

MicrosoftUserCredentialsConfigurationDTO.

None
custom WriteBlock

CustomUserCredentialsConfigurationDTO (username, active…).

None

edit_credentials_raw(user_id, req)

PUT /admin/manage/users/{userId}/credentials da un DTO già costruito.

edit_raw(user_id, req)

PUT /admin/manage/users/{userId} da un DTO già costruito (occToken compreso).

get_credentials_for_edit(user_id)

GET /admin/manage/users/{userId}/credentials/edit.

Alias di :meth:~pynteracta.api.admin_manage.AdminManageAPI.user_credentials_for_edit: stessa richiesta, stessa façade, con occ_token per :meth:edit_credentials.

get_for_edit(user_id)

GET /admin/manage/users/{userId}/edit — requires admin permissions.

iterate(*, page_size=None, **filters)

Lazy iterator over all pages of :meth:list.

list(*, page_token=None, page_size=None, calculate_total_items_count=None, full_text_filter=None, status_filter=None, workspace_ids=None, community_ids=None, creation_timestamp_from=None, creation_timestamp_to=None, last_access_timestamp_from=None, last_access_timestamp_to=None, role=None, order_by=None, order_desc=None, **filters)

POST /admin/data/users with curated filter and ordering kwargs.

Parameters:

Name Type Description Default
page_token str | None

Opaque token for the page to fetch.

None
page_size int | None

Items per page.

None
calculate_total_items_count bool | None

Ask the API to populate totalItemsCount.

None
full_text_filter str | None

Full-text filter on name, surname, and email (fullTextFilter).

None
status_filter list[int] | None

Filter by user status ids (statusFilter).

None
workspace_ids list[int] | None

Filter by workspace ids (workspaceIds).

None
community_ids list[int] | None

Filter by community ids (communityIds).

None
creation_timestamp_from int | float | str | datetime | None

Lower bound on creation date — epoch-ms int, :class:~datetime.datetime, or ISO-8601 str (creationTimestampFrom).

None
creation_timestamp_to int | float | str | datetime | None

Upper bound on creation date (creationTimestampTo).

None
last_access_timestamp_from int | float | str | datetime | None

Lower bound on last-access date (lastAccessTimestampFrom).

None
last_access_timestamp_to int | float | str | datetime | None

Upper bound on last-access date (lastAccessTimestampTo).

None
role str | None

Filter by role (role).

None
order_by str | None

Sort field id — mapped to orderTypeId (passthrough, no client-side validation).

None
order_desc bool | None

Descending sort (True) or ascending (False) — orderDesc.

None
**filters Any

Additional snake_case fields forwarded to the request body (converted to camelCase). Escape hatch for the long-tail ListSystemUsersRequestDTO fields not promoted to explicit kwargs (e.g. business_unit_ids, area_ids, manager_ids, lang, login_provider_filter, name/email prefixes).

{}

list_raw(req)

POST /admin/data/users with a pre-built request DTO (escape hatch).

me()

GET /core/auth/current-user-data — alias for :meth:AuthAPI.current_user_data.

profile()

GET /core/user-profile/info — own profile (distinct from :meth:me).

Facade Reference

pynteracta.models.facade.users.UserForEdit

Narrow facade over :class:~generated.GetUserForEditResponseDTO.

Attributes:

Name Type Description
raw

The underlying generated DTO; access additional fields via this escape hatch.

Source code in src/pynteracta/models/facade/users.py
class UserForEdit:
    """Narrow facade over :class:`~generated.GetUserForEditResponseDTO`.

    Attributes:
        raw: The underlying generated DTO; access additional fields via this escape hatch.
    """

    def __init__(self, raw: generated.GetUserForEditResponseDTO) -> None:
        self.raw = raw

    @property
    def first_name(self) -> str | None:
        """User first name."""
        return self.raw.firstname

    @property
    def last_name(self) -> str | None:
        """User last name."""
        return self.raw.lastname

    @property
    def contact_email(self) -> str | None:
        """Primary contact email."""
        return self.raw.contactEmail

    @property
    def external_id(self) -> str | None:
        """External system identifier."""
        return self.raw.externalId

    @property
    def blocked(self) -> bool | None:
        """Whether the account is blocked."""
        return self.raw.blocked

    @property
    def account_photo_url(self) -> str | None:
        """URL of the user's current profile photo."""
        return self.raw.accountPhotoUrl

    @property
    def occ_token(self) -> int | None:
        """Token di concorrenza da passare a ``edit`` (spec 05, RF-023a)."""
        return self.raw.occToken

    @classmethod
    def from_dict(cls, data: dict) -> UserForEdit:  # type: ignore[type-arg]
        """Parse from a raw API response dict.

        Args:
            data: Parsed JSON response body.

        Returns:
            A new :class:`UserForEdit`.
        """
        raw = generated.GetUserForEditResponseDTO.model_validate(data)
        return cls(raw)

account_photo_url property

URL of the user's current profile photo.

blocked property

Whether the account is blocked.

contact_email property

Primary contact email.

external_id property

External system identifier.

first_name property

User first name.

last_name property

User last name.

occ_token property

Token di concorrenza da passare a edit (spec 05, RF-023a).

from_dict(data) classmethod

Parse from a raw API response dict.

Parameters:

Name Type Description Default
data dict

Parsed JSON response body.

required

Returns:

Type Description
UserForEdit

A new :class:UserForEdit.

Source code in src/pynteracta/models/facade/users.py
@classmethod
def from_dict(cls, data: dict) -> UserForEdit:  # type: ignore[type-arg]
    """Parse from a raw API response dict.

    Args:
        data: Parsed JSON response body.

    Returns:
        A new :class:`UserForEdit`.
    """
    raw = generated.GetUserForEditResponseDTO.model_validate(data)
    return cls(raw)

pynteracta.models.facade.users.UserWriteResult

Façade sulla risposta di create, edit ed edit_credentials di un utente.

I tre DTO di risposta non hanno gli stessi campi: le proprietà assenti valgono None. user_id viene dalla risposta di creazione; per le modifiche è quello passato dal chiamante, perché il server non lo restituisce.

Attributes:

Name Type Description
raw

Il DTO generato sottostante (CreateUserResponseDTO, EditUserResponseDTO o EditUserCredentialsResponseDTO).

Source code in src/pynteracta/models/facade/users.py
class UserWriteResult:
    """Façade sulla risposta di ``create``, ``edit`` ed ``edit_credentials`` di un utente.

    I tre DTO di risposta non hanno gli stessi campi: le proprietà assenti valgono ``None``.
    ``user_id`` viene dalla risposta di creazione; per le modifiche è quello passato dal
    chiamante, perché il server non lo restituisce.

    Attributes:
        raw: Il DTO generato sottostante (``CreateUserResponseDTO``, ``EditUserResponseDTO`` o
            ``EditUserCredentialsResponseDTO``).
    """

    def __init__(
        self,
        raw: (
            generated.CreateUserResponseDTO
            | generated.EditUserResponseDTO
            | generated.EditUserCredentialsResponseDTO
        ),
        *,
        user_id: int | None = None,
    ) -> None:
        self.raw = raw
        self._user_id = user_id

    @property
    def user_id(self) -> int | None:
        """Id dell'utente scritto: dalla risposta di creazione, altrimenti quello passato."""
        user_id = getattr(self.raw, "userId", None)
        if user_id is not None:
            return int(user_id)
        return self._user_id

    @property
    def next_occ_token(self) -> int | None:
        """Token di concorrenza da usare per la modifica successiva."""
        return self.raw.nextOccToken

    @property
    def generated_password(self) -> list[str] | None:
        """Password custom generata dal server alla creazione, se richiesta."""
        value: list[str] | None = getattr(self.raw, "generatedPassword", None)
        return value

    @property
    def expired_credentials(self) -> bool | None:
        """Se le credenziali custom create vanno cambiate al primo accesso."""
        value: bool | None = getattr(self.raw, "expiredCredentials", None)
        return value

    @property
    def sent_email_notify(self) -> bool | None:
        """Se il server ha inviato l'email di notifica delle credenziali."""
        value: bool | None = getattr(self.raw, "sentEmailNotify", None)
        return value

    @property
    def account_photo_url(self) -> str | None:
        """URL della foto dell'utente dopo la scrittura."""
        value: str | None = getattr(self.raw, "accountPhotoUrl", None)
        return value

    @classmethod
    def from_create(cls, data: dict[str, Any]) -> UserWriteResult:
        """Legge la risposta di ``POST admin/manage/users``.

        Lo swagger dichiara ``generatedPassword`` come lista di stringhe, ma il tenant restituisce
        una stringa (prova sul tenant, spec 05): la si normalizza in lista di un elemento. Se la
        risposta non ha la forma attesa l'errore riporta solo i nomi dei campi, mai i valori, e
        non incatena l'eccezione di pydantic: la password non deve finire in un messaggio di
        eccezione (RNF-001).
        """
        payload = dict(data)
        password = payload.get("generatedPassword")
        if isinstance(password, str):
            payload["generatedPassword"] = [password]
        try:
            raw = generated.CreateUserResponseDTO.model_validate(payload)
        except PydanticValidationError as exc:
            fields = sorted(
                {".".join(str(part) for part in err["loc"]) or "<root>" for err in exc.errors()}
            )
        else:
            return cls(raw)
        msg = f"Unexpected create-user response: invalid field(s) {', '.join(fields)}"
        raise ValueError(msg)

    @classmethod
    def from_edit(cls, data: dict[str, Any], user_id: int) -> UserWriteResult:
        """Legge la risposta di ``PUT admin/manage/users/{userId}``."""
        return cls(generated.EditUserResponseDTO.model_validate(data), user_id=user_id)

    @classmethod
    def from_credentials(cls, data: dict[str, Any], user_id: int) -> UserWriteResult:
        """Legge la risposta di ``PUT admin/manage/users/{userId}/credentials``."""
        return cls(generated.EditUserCredentialsResponseDTO.model_validate(data), user_id=user_id)

account_photo_url property

URL della foto dell'utente dopo la scrittura.

expired_credentials property

Se le credenziali custom create vanno cambiate al primo accesso.

generated_password property

Password custom generata dal server alla creazione, se richiesta.

next_occ_token property

Token di concorrenza da usare per la modifica successiva.

sent_email_notify property

Se il server ha inviato l'email di notifica delle credenziali.

user_id property

Id dell'utente scritto: dalla risposta di creazione, altrimenti quello passato.

from_create(data) classmethod

Legge la risposta di POST admin/manage/users.

Lo swagger dichiara generatedPassword come lista di stringhe, ma il tenant restituisce una stringa (prova sul tenant, spec 05): la si normalizza in lista di un elemento. Se la risposta non ha la forma attesa l'errore riporta solo i nomi dei campi, mai i valori, e non incatena l'eccezione di pydantic: la password non deve finire in un messaggio di eccezione (RNF-001).

Source code in src/pynteracta/models/facade/users.py
@classmethod
def from_create(cls, data: dict[str, Any]) -> UserWriteResult:
    """Legge la risposta di ``POST admin/manage/users``.

    Lo swagger dichiara ``generatedPassword`` come lista di stringhe, ma il tenant restituisce
    una stringa (prova sul tenant, spec 05): la si normalizza in lista di un elemento. Se la
    risposta non ha la forma attesa l'errore riporta solo i nomi dei campi, mai i valori, e
    non incatena l'eccezione di pydantic: la password non deve finire in un messaggio di
    eccezione (RNF-001).
    """
    payload = dict(data)
    password = payload.get("generatedPassword")
    if isinstance(password, str):
        payload["generatedPassword"] = [password]
    try:
        raw = generated.CreateUserResponseDTO.model_validate(payload)
    except PydanticValidationError as exc:
        fields = sorted(
            {".".join(str(part) for part in err["loc"]) or "<root>" for err in exc.errors()}
        )
    else:
        return cls(raw)
    msg = f"Unexpected create-user response: invalid field(s) {', '.join(fields)}"
    raise ValueError(msg)

from_credentials(data, user_id) classmethod

Legge la risposta di PUT admin/manage/users/{userId}/credentials.

Source code in src/pynteracta/models/facade/users.py
@classmethod
def from_credentials(cls, data: dict[str, Any], user_id: int) -> UserWriteResult:
    """Legge la risposta di ``PUT admin/manage/users/{userId}/credentials``."""
    return cls(generated.EditUserCredentialsResponseDTO.model_validate(data), user_id=user_id)

from_edit(data, user_id) classmethod

Legge la risposta di PUT admin/manage/users/{userId}.

Source code in src/pynteracta/models/facade/users.py
@classmethod
def from_edit(cls, data: dict[str, Any], user_id: int) -> UserWriteResult:
    """Legge la risposta di ``PUT admin/manage/users/{userId}``."""
    return cls(generated.EditUserResponseDTO.model_validate(data), user_id=user_id)

See also the guide Filtering and sorting users.