Skip to content

models

PlantDB Commons Authentication Models This module defines core authentication primitives: Permission, Role, User, and Group classes for a plant database system. It provides fine‑grained permission management, role‑based access control, user serialization, and group membership utilities.

Key Features
  • Enumerations for granular permissions and role‑based access control.
  • User dataclass with serialization (to_dict, to_json, etc.), authentication helpers and lockout logic.
  • Group dataclass for collaborative access to scan datasets with user membership management.
Usage Examples

from plantdb.commons.auth.models import Permission, Role, User, Group from datetime import datetime, timezone user = User(username="alice", fullname="Alice Smith", password_hash="hashed_pw", roles={Role.CONTRIBUTOR}, created_at=datetime.now(timezone.utc)) print(user.roles) {} group = Group(name="researchers", users={"alice"}, created_at=datetime.now(timezone.utc), created_by="alice") group.add_user("bob") True

Group dataclass Link

Group(name, users, created_at, created_by, description=None)

Represents a group of users for sharing scan datasets.

Groups allow multiple users to collaborate on scan datasets. When a scan is shared with a group, all members of that group get the CONTRIBUTOR role for that specific dataset.

Attributes:

Name Type Description
name str

The unique name of the group.

users Set[str]

A set of usernames that belong to this group.

description Optional[str]

An optional description of the group's purpose.

created_at datetime

The timestamp when the group was created.

created_by str

The username of the user who created the group.

Examples:

>>> from datetime import datetime, timezone
>>> from plantdb.commons.auth.models import Group
>>> group = Group(
...     name="plant_researchers",
...     users={"alice", "bob"},
...     description="Plant research team",
...     created_at=datetime.now(timezone.utc),
...     created_by="alice"
... )
>>> print(group.name)
plant_researchers
>>> "alice" in group.users
True

add_user Link

add_user(username)

Add a user to the group.

Parameters:

Name Type Description Default

username Link

str

The username to add to the group.

required

Returns:

Type Description
bool

True if the user was added (wasn't already in the group), False otherwise.

Source code in plantdb/commons/auth/models.py
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
def add_user(self, username: str) -> bool:
    """Add a user to the group.

    Parameters
    ----------
    username : str
        The username to add to the group.

    Returns
    -------
    bool
        True if the user was added (wasn't already in the group), False otherwise.
    """
    if username in self.users:
        return False
    self.users.add(username)
    return True

has_user Link

has_user(username)

Check if a user is a member of the group.

Parameters:

Name Type Description Default

username Link

str

The username to check.

required

Returns:

Type Description
bool

True if the user is a member of the group, False otherwise.

Source code in plantdb/commons/auth/models.py
871
872
873
874
875
876
877
878
879
880
881
882
883
884
def has_user(self, username: str) -> bool:
    """Check if a user is a member of the group.

    Parameters
    ----------
    username : str
        The username to check.

    Returns
    -------
    bool
        True if the user is a member of the group, False otherwise.
    """
    return username in self.users

remove_user Link

remove_user(username)

Remove a user from the group.

Parameters:

Name Type Description Default

username Link

str

The username to remove from the group.

required

Returns:

Type Description
bool

True if the user was removed (was in the group), False otherwise.

Source code in plantdb/commons/auth/models.py
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
def remove_user(self, username: str) -> bool:
    """Remove a user from the group.

    Parameters
    ----------
    username : str
        The username to remove from the group.

    Returns
    -------
    bool
        True if the user was removed (was in the group), False otherwise.
    """
    if username not in self.users:
        return False
    self.users.remove(username)
    return True

Permission Link

Bases: Enum

A class representing the different permission levels for user actions.

This enumeration defines various permissions that can be assigned to users, controlling their access to different functionalities within the system.

Attributes:

Name Type Description
READ str

Permission to read scan data and metadata.

WRITE str

Permission to write filesets, files, and associated metadata in existing scan datasets.

CREATE str

Permission to create new scan datasets.

DELETE str

Permission to delete scan datasets, filesets, and files.

MANAGE_USERS str

Permission to add and remove users.

MANAGE_GROUPS str

Permission to manage groups (create, delete, modify membership).

Examples:

>>> from plantdb.commons.auth.models import Permission
Accessing a specific permission:
>>> print(Permission.READ)
Permission.READ
Iterating through all permissions:
>>> for perm in Permission:
...     print(f"{perm.name}: {perm.value}")
READ: read
WRITE: write
CREATE: create
DELETE: delete
MANAGE_USERS: manage_users
MANAGE_GROUPS: manage_groups

__eq__ Link

__eq__(other)

Equality operator.

Parameters:

Name Type Description Default

other Link

Any

Object to compare with the permission instance. If a string is provided, it is interpreted as a permission name and converted to a Permission instance for comparison.

required

Returns:

Type Description
bool

True if other represents the same permission as self, otherwise False.

Notes

The method safely handles objects that do not expose a value attribute. This ensures that comparisons with unrelated types do not raise unexpected exceptions.

Source code in plantdb/commons/auth/models.py
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
def __eq__(self, other: Any) -> bool:
    """Equality operator.

    Parameters
    ----------
    other : Any
        Object to compare with the permission instance.
        If a string is provided, it is interpreted as a permission name and converted to a
        ``Permission`` instance for comparison.

    Returns
    -------
    bool
        ``True`` if ``other`` represents the same permission as ``self``, otherwise ``False``.

    Notes
    -----
    The method safely handles objects that do not expose a ``value`` attribute.
    This ensures that comparisons with unrelated types do not raise unexpected exceptions.
    """
    if isinstance(other, str):
        try:
            return Permission.from_string(other) == self
        except ValueError:
            return False
    else:
        try:
            return other.value == self.value
        except AttributeError:
            return False

from_string classmethod Link

from_string(s)

Convert a string to a Permission member.

The function is tolerant of the following inputs: * the plain enum value ("read"); * the plain enum name ("READ"); * a fully‑qualified name ("Permission.READ").

Parameters:

Name Type Description Default

s Link

str

The string representation supplied by the caller.

required

Returns:

Type Description
Permission

The matching enum member.

Raises:

Type Description
ValueError

If s does not correspond to any defined permission.

Examples:

>>> from plantdb.commons.auth.models import Permission
>>> perm = Permission.from_string('READ')
>>> print(perm)
Permission.READ
Source code in plantdb/commons/auth/models.py
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
@classmethod
def from_string(cls, s: str) -> "Permission":
    """Convert a string to a `Permission` member.

    The function is tolerant of the following inputs:
    * the plain enum *value* (`"read"`);
    * the plain enum *name* (`"READ"`);
    * a fully‑qualified name (`"Permission.READ"`).

    Parameters
    ----------
    s: str
        The string representation supplied by the caller.

    Returns
    -------
    plantdb.commons.auth.models.Permission
        The matching enum member.

    Raises
    ------
    ValueError
        If ``s`` does not correspond to any defined permission.

    Examples
    --------
    >>> from plantdb.commons.auth.models import Permission
    >>> perm = Permission.from_string('READ')
    >>> print(perm)
    Permission.READ
    """
    # strip any surrounding whitespace.
    s = s.strip()

    # If the string contains a dot (e.g. "Permission.READ"), keep only the part after the last dot.
    if "." in s:
        s = s.split(".")[-1]

    # Try to resolve by *name* first (case‑insensitive)
    name_key = s.upper()
    if name_key in cls.__members__:  # ``cls.__members__`` holds a mapping of names → members
        return cls[name_key]

    # If that fails, try to resolve by *value*.
    try:
        return cls(s)
    except ValueError as exc:
        # ``cls(s)`` raises ValueError automatically if not found.
        raise ValueError(f"Unknown permission string: {s!r}") from exc

Role Link

Bases: Enum

A class representing the role of a user in a system.

The Role enum defines three types of roles that can be assigned to users:

* READER: Has read-only access.
* CONTRIBUTOR: Can modify content but not delete.
* ADMIN: Full control over all aspects of the system.

This enum helps in managing permissions and access levels efficiently within an application.

Attributes:

Name Type Description
READER str

Represents a user with read-only access.

CONTRIBUTOR str

Represents a user who can modify content but not delete it.

ADMIN str

Represents a user with full control over the system.

Examples:

>>> from plantdb.commons.auth.models import Role
>>> Role.READER
<Role.READER: 'reader'>
>>> Role.CONTRIBUTOR
<Role.CONTRIBUTOR: 'contributor'>
>>> Role.ADMIN
<Role.ADMIN: 'admin'>
>>> Role.ADMIN.permissions
{<Permission.CREATE: 'create'>,
 <Permission.DELETE: 'delete'>,
 <Permission.MANAGE_GROUPS: 'manage_groups'>,
 <Permission.MANAGE_USERS: 'manage_users'>,
 <Permission.READ: 'read'>,
 <Permission.WRITE: 'write'>}

permissions property Link

permissions

Get the set of permissions associated with this role.

Returns:

Type Description
Set[Permission]

A set containing all permissions granted to this role.

Examples:

>>> from plantdb.commons.auth.models import Role
>>> guest = Role.READER
>>> guest.permissions
{<Permission.READ: 'read'>}
>>> user = Role.CONTRIBUTOR
>>> user.permissions
{<Permission.CREATE: 'create'>,
 <Permission.READ: 'read'>,
 <Permission.WRITE: 'write'>}

rank property Link

rank

Get the hierarchical rank of the role. Higher is more powerful.

Returns:

Type Description
int

The rank of the role.

Examples:

>>> from plantdb.commons.auth.models import Role
>>> guest = Role.READER
>>> guest.rank
1

can_assign Link

can_assign(target_role)

Check if this role has the authority to assign the target_role.

A user can only assign roles that are less than or equal to their own.

Returns:

Type Description
bool

True if this role has the authority to assign the target_role; False otherwise.

Examples:

>>> from plantdb.commons.auth.models import Role
>>> guest = Role.READER
>>> guest.can_assign(Role.CONTRIBUTOR)
False
>>> user = Role.CONTRIBUTOR
>>> user.can_assign(Role.CONTRIBUTOR)
True
Source code in plantdb/commons/auth/models.py
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
def can_assign(self, target_role: 'Role') -> bool:
    """Check if this role has the authority to assign the target_role.

    A user can only assign roles that are less than or equal to their own.

    Returns
    -------
    bool
        ``True`` if this role has the authority to assign the target_role; ``False`` otherwise.

    Examples
    --------
    >>> from plantdb.commons.auth.models import Role
    >>> guest = Role.READER
    >>> guest.can_assign(Role.CONTRIBUTOR)
    False
    >>> user = Role.CONTRIBUTOR
    >>> user.can_assign(Role.CONTRIBUTOR)
    True
    """
    return self.rank >= target_role.rank

TokenUser dataclass Link

TokenUser(username, fullname, password_hash, created_at, roles=set(), last_login=None, is_active=True, failed_attempts=0, last_failed_attempt=None, locked_until=None, password_last_change=None, _extra_permissions=set(), dataset_permissions=None)

Bases: User

A user representation that includes per‑dataset permission mappings derived from a token.

The TokenUser subclass augments the User class with an optional dataset_permissions attribute that maps glob‑style dataset name patterns to a set of Permission objects.

Attributes:

Name Type Description
username str

The unique username of the user.

password_hash str

The hashed password of the user.

roles Set[Role]

A set containing roles assigned to the user.

created_at datetime

The timestamp when the user account was created.

permissions Optional[Set[Permission]]

A set containing specific permissions granted to the user.

last_login Optional[datetime]

The timestamp of the last login. If not provided, defaults to None.

is_active bool

Indicates if the user account is active. Defaults to True.

failed_attempts int

Number of failed login attempts for the user. Defaults to 0.

last_failed_attempt Optional[datetime]

The timestamp of the last failed attempts. Defaults to None.

locked_until Optional[datetime]

Timestamp until which the user is locked out due to multiple failed attempts. Defaults to None.

password_last_change Optional[datetime]

The timestamp of the last password change. Defaults to None.

dataset_permissions dict[str, set[Permission]]

Mapping from dataset name patterns (e.g., "proj_*") to a set of Permission objects granting access to those datasets.

Examples:

>>> from datetime import datetime, timezone
>>> from plantdb.commons.auth.models import Permission, Role, User, TokenUser
>>> # Create a TokenUser from an existing User:
>>> user = User(username="jdoe", fullname="John Doe", password_hash="hashed_password", roles={Role.CONTRIBUTOR}, created_at=datetime.now(timezone.utc))
>>> token_user = TokenUser(**user.to_dict(), dataset_permissions={'new_scan': {Permission.WRITE, Permission.DELETE}})
>>> token_user.permissions
{<Permission.CREATE: 'create'>, <Permission.READ: 'read'>, <Permission.WRITE: 'write'>}
>>> token_user.get_permissions_for_dataset('new_scan')
{<Permission.WRITE: 'write'>}

permissions property Link

permissions

All effective permissions = role‑derived ∪ extra.

__eq__ Link

__eq__(other)

Compare two User objects for equality.

Two User objects are considered equal if all their attributes have the same values.

Parameters:

Name Type Description Default

other Link

Any

The object to compare with.

required

Returns:

Type Description
bool

True if the objects are equal, False otherwise.

Source code in plantdb/commons/auth/models.py
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def __eq__(self, other):
    """Compare two ``User`` objects for equality.

    Two ``User`` objects are considered equal if all their attributes have the same values.

    Parameters
    ----------
    other : Any
        The object to compare with.

    Returns
    -------
    bool
        ``True`` if the objects are equal, ``False`` otherwise.
    """
    if not isinstance(other, User):
        return False

    return all(self.__dict__[attr] == other.__dict__[attr] for attr in self.__dict__)

__post_init__ Link

__post_init__()

Validates that the dataset permissions mapping is supplied.

Raises:

Type Description
TypeError

If the dataset_permissions mapping is missing.

Source code in plantdb/commons/auth/models.py
709
710
711
712
713
714
715
716
717
718
719
720
721
722
def __post_init__(self):
    """Validates that the dataset permissions mapping is supplied.

    Raises
    ------
    TypeError
        If the ``dataset_permissions`` mapping is missing.
    """
    # ---- roles type validation -------------------------------------
    self.roles = {Role(role) if not isinstance(role, Role) else role for role in self.roles}
    self._extra_permissions = {Role(perm) if not isinstance(perm, Permission) else perm for perm in self._extra_permissions}
    # ---- dataset_permissions validation ----------------------------
    if not self.dataset_permissions:
        raise TypeError("`dataset_permissions` argument must be provided.")

from_dict classmethod Link

from_dict(data)

Create a User object from a dictionary (JSON deserialization).

Parameters:

Name Type Description Default

data Link

dict

Dictionary containing user data.

required

Returns:

Type Description
TokenUser

The TokenUser object created from the dictionary data.

Raises:

Type Description
ValueError

If required fields are missing or invalid.

KeyError

If required keys are missing from the dictionary.

Source code in plantdb/commons/auth/models.py
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
@classmethod
def from_dict(cls, data: dict) -> 'User':
    """Create a ``User`` object from a dictionary (JSON deserialization).

    Parameters
    ----------
    data : dict
        Dictionary containing user data.

    Returns
    -------
    TokenUser
        The ``TokenUser`` object created from the dictionary data.

    Raises
    ------
    ValueError
        If required fields are missing or invalid.
    KeyError
        If required keys are missing from the dictionary.
    """
    args = cls._parse_user_dict(data)

    raw_dataset_perms = data.get("dataset_permissions")
    if raw_dataset_perms is None:
        raise KeyError("Missing required field 'dataset_permissions' in token user data")

    args["dataset_permissions"] = {
        dname: {Permission(perm) for perm in perms}
        for dname, perms in raw_dataset_perms.items()
    }

    return cls(**args)

from_json classmethod Link

from_json(json_str)

Create a User object from JSON string.

Parameters:

Name Type Description Default

json_str Link

str

JSON string containing user data.

required

Returns:

Type Description
User

The User object created from the JSON data.

Raises:

Type Description
JSONDecodeError

If the JSON string is invalid.

ValueError

If required fields are missing or invalid.

Examples:

>>> import json
>>> from plantdb.commons.auth.models import User
Source code in plantdb/commons/auth/models.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
@classmethod
def from_json(cls, json_str: str) -> 'User':
    """Create a ``User`` object from JSON string.

    Parameters
    ----------
    json_str : str
        JSON string containing user data.

    Returns
    -------
    User
        The ``User`` object created from the JSON data.

    Raises
    ------
    json.JSONDecodeError
        If the JSON string is invalid.
    ValueError
        If required fields are missing or invalid.

    Examples
    --------
    >>> import json
    >>> from plantdb.commons.auth.models import User
    """
    try:
        data = json.loads(json_str)
        return cls.from_dict(data)
    except json.JSONDecodeError as e:
        raise json.JSONDecodeError(f"Invalid JSON format: {e}", json_str, e.pos)

get_permissions_for_dataset Link

get_permissions_for_dataset(dataset_name)

Returns the set of permissions available for this TokenUser for the provided dataset name.

Parameters:

Name Type Description Default

dataset_name Link

str

Name of the dataset.

required

Returns:

Type Description
set[Permission]

Set of available permissions for this TokenUser for the provided dataset name.

Source code in plantdb/commons/auth/models.py
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
def get_permissions_for_dataset(self, dataset_name: str) -> set[Permission]:
    """Returns the set of permissions available for this TokenUser for the provided dataset name.

    Parameters
    ----------
    dataset_name: str
        Name of the dataset.

    Returns
    -------
    set[Permission]
        Set of available permissions for this TokenUser for the provided dataset name.
    """
    permissions = set()
    def_perm = self.permissions
    for dataset_pattern, _permissions in self.dataset_permissions.items():
        # Check if the dataset name matches the glob‑style pattern.
        if fnmatchcase(dataset_name, dataset_pattern):
            # Intersect pattern permissions with the user's global permissions and add them to the result set.
            permissions |= _permissions & def_perm
    return permissions

grant_permission Link

grant_permission(perm)

Add a permission not covered by any current role.

Source code in plantdb/commons/auth/models.py
476
477
478
479
def grant_permission(self, perm: Permission) -> None:
    """Add a permission not covered by any current role."""
    if perm not in self.permissions:  # avoid duplicates
        self._extra_permissions.add(perm)

revoke_permission Link

revoke_permission(perm)

Remove an explicitly‑granted permission (won’t affect role‑derived ones).

Source code in plantdb/commons/auth/models.py
481
482
483
def revoke_permission(self, perm: Permission) -> None:
    """Remove an explicitly‑granted permission (won’t affect role‑derived ones)."""
    self._extra_permissions.discard(perm)

to_dict Link

to_dict()

Convert a TokenUser object to a dictionary for JSON serialization.

Returns:

Type Description
dict

Dictionary representation of the TokenUser object.

Source code in plantdb/commons/auth/models.py
724
725
726
727
728
729
730
731
732
733
734
def to_dict(self) -> dict:
    """Convert a ``TokenUser`` object to a dictionary for JSON serialization.

    Returns
    -------
    dict
        Dictionary representation of the ``TokenUser`` object.
    """
    d = super().to_dict()
    d["dataset_permissions"] = self.dataset_permissions
    return d

to_json Link

to_json()

Convert User object to JSON string.

Returns:

Type Description
str

JSON string representation of the User object.

Source code in plantdb/commons/auth/models.py
584
585
586
587
588
589
590
591
592
def to_json(self) -> str:
    """Convert ``User`` object to JSON string.

    Returns
    -------
    str
        JSON string representation of the ``User`` object.
    """
    return json.dumps(self.to_dict(), indent=2)

User dataclass Link

User(username, fullname, password_hash, created_at, roles=set(), last_login=None, is_active=True, failed_attempts=0, last_failed_attempt=None, locked_until=None, password_last_change=None, _extra_permissions=set())

Represents a user entity in the application.

It contains attributes related to user authentication, roles, permissions, and activity timestamps. The class is designed to encapsulate user data and provide methods for user management. Users can have multiple roles and permissions, which are stored as sets. The class also tracks the creation time and last login time of a user.

Attributes:

Name Type Description
username str

The unique username of the user.

password_hash str

The hashed password of the user.

roles Set[Role]

A set containing roles assigned to the user.

created_at datetime

The timestamp when the user account was created.

last_login Optional[datetime]

The timestamp of the last login. If not provided, defaults to None.

is_active bool

Indicates if the user account is active. Defaults to True.

failed_attempts int

Number of failed login attempts for the user. Defaults to 0.

last_failed_attempt Optional[datetime]

The timestamp of the last failed attempts. Defaults to None.

locked_until Optional[datetime]

Timestamp until which the user is locked out due to multiple failed attempts. Defaults to None.

password_last_change Optional[datetime]

The timestamp of the last password change. Defaults to None.

_extra_permissions Set[Permission]

A set containing specific permissions granted to the user, not covered by its role.

Notes

Ensure that sensitive data like password_hash is handled securely and not exposed in logs or error messages.

Examples:

>>> from datetime import datetime, timezone
>>> from plantdb.commons.auth.models import Permission, Role, User
>>> user = User(
...     username="jdoe",
...     fullname="John Doe",
...     password_hash="hashed_password",
...     roles={Role.CONTRIBUTOR},
...     created_at=datetime.now(timezone.utc),
... )
>>> print(user.username)
jdoe
>>> print(user.roles)  # get roles
{<Role.CONTRIBUTOR: 'contributor'>}
>>> print(user.permissions)
{<Permission.CREATE: 'create'>, <Permission.READ: 'read'>, <Permission.WRITE: 'write'>}
>>> user.grant_permission(Permission.MANAGE_USERS)
>>> print(user.permissions)
{<Permission.CREATE: 'create'>, <Permission.READ: 'read'>, <Permission.MANAGE_USERS: 'manage_users'>, <Permission.WRITE: 'write'>}
>>> user.last_login = datetime.now(timezone.utc)  # set the timestamp when the user account was last login

permissions property Link

permissions

All effective permissions = role‑derived ∪ extra.

__eq__ Link

__eq__(other)

Compare two User objects for equality.

Two User objects are considered equal if all their attributes have the same values.

Parameters:

Name Type Description Default

other Link

Any

The object to compare with.

required

Returns:

Type Description
bool

True if the objects are equal, False otherwise.

Source code in plantdb/commons/auth/models.py
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
def __eq__(self, other):
    """Compare two ``User`` objects for equality.

    Two ``User`` objects are considered equal if all their attributes have the same values.

    Parameters
    ----------
    other : Any
        The object to compare with.

    Returns
    -------
    bool
        ``True`` if the objects are equal, ``False`` otherwise.
    """
    if not isinstance(other, User):
        return False

    return all(self.__dict__[attr] == other.__dict__[attr] for attr in self.__dict__)

from_dict classmethod Link

from_dict(data)

Create a User object from a dictionary (JSON deserialization).

Parameters:

Name Type Description Default

data Link

dict

Dictionary containing user data.

required

Returns:

Type Description
User

The User object created from the dictionary data.

Raises:

Type Description
ValueError

If required fields are missing or invalid.

KeyError

If required keys are missing from the dictionary.

Source code in plantdb/commons/auth/models.py
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
@classmethod
def from_dict(cls, data: dict) -> 'User':
    """Create a ``User`` object from a dictionary (JSON deserialization).

    Parameters
    ----------
    data : dict
        Dictionary containing user data.

    Returns
    -------
    User
        The ``User`` object created from the dictionary data.

    Raises
    ------
    ValueError
        If required fields are missing or invalid.
    KeyError
        If required keys are missing from the dictionary.
    """
    args = cls._parse_user_dict(data)
    return cls(**args)

from_json classmethod Link

from_json(json_str)

Create a User object from JSON string.

Parameters:

Name Type Description Default

json_str Link

str

JSON string containing user data.

required

Returns:

Type Description
User

The User object created from the JSON data.

Raises:

Type Description
JSONDecodeError

If the JSON string is invalid.

ValueError

If required fields are missing or invalid.

Examples:

>>> import json
>>> from plantdb.commons.auth.models import User
Source code in plantdb/commons/auth/models.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
@classmethod
def from_json(cls, json_str: str) -> 'User':
    """Create a ``User`` object from JSON string.

    Parameters
    ----------
    json_str : str
        JSON string containing user data.

    Returns
    -------
    User
        The ``User`` object created from the JSON data.

    Raises
    ------
    json.JSONDecodeError
        If the JSON string is invalid.
    ValueError
        If required fields are missing or invalid.

    Examples
    --------
    >>> import json
    >>> from plantdb.commons.auth.models import User
    """
    try:
        data = json.loads(json_str)
        return cls.from_dict(data)
    except json.JSONDecodeError as e:
        raise json.JSONDecodeError(f"Invalid JSON format: {e}", json_str, e.pos)

grant_permission Link

grant_permission(perm)

Add a permission not covered by any current role.

Source code in plantdb/commons/auth/models.py
476
477
478
479
def grant_permission(self, perm: Permission) -> None:
    """Add a permission not covered by any current role."""
    if perm not in self.permissions:  # avoid duplicates
        self._extra_permissions.add(perm)

revoke_permission Link

revoke_permission(perm)

Remove an explicitly‑granted permission (won’t affect role‑derived ones).

Source code in plantdb/commons/auth/models.py
481
482
483
def revoke_permission(self, perm: Permission) -> None:
    """Remove an explicitly‑granted permission (won’t affect role‑derived ones)."""
    self._extra_permissions.discard(perm)

to_dict Link

to_dict()

Convert a User object to a dictionary for JSON serialization.

Returns:

Type Description
dict

Dictionary representation of the User object.

Source code in plantdb/commons/auth/models.py
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
def to_dict(self) -> dict:
    """Convert a ``User`` object to a dictionary for JSON serialization.

    Returns
    -------
    dict
        Dictionary representation of the ``User`` object.
    """
    return {
        'username': self.username,
        'fullname': self.fullname,
        'password_hash': self.password_hash,
        'roles': [role.value for role in self.roles],
        'created_at': self.created_at.isoformat(),
        '_extra_permissions': [perm.value for perm in self._extra_permissions],
        'last_login': self.last_login.isoformat() if self.last_login else None,
        'is_active': self.is_active,
        'failed_attempts': self.failed_attempts,
        'last_failed_attempt': self.last_failed_attempt.isoformat() if self.last_failed_attempt else None,
        'locked_until': self.locked_until.isoformat() if self.locked_until else None,
        'password_last_change': self.password_last_change.isoformat() if self.password_last_change else None,
    }

to_json Link

to_json()

Convert User object to JSON string.

Returns:

Type Description
str

JSON string representation of the User object.

Source code in plantdb/commons/auth/models.py
584
585
586
587
588
589
590
591
592
def to_json(self) -> str:
    """Convert ``User`` object to JSON string.

    Returns
    -------
    str
        JSON string representation of the ``User`` object.
    """
    return json.dumps(self.to_dict(), indent=2)

dataset_perm_to_str Link

dataset_perm_to_str(dataset_perm)

Convert a mapping of dataset names to permission lists into a compact string representation.

Parameters:

Name Type Description Default

dataset_perm Link

Dict[str, list[Permission]]

Mapping from dataset identifiers (strings) to a list of Permission objects that define the access rights for each dataset.

required

Returns:

Type Description
str

A single string where each dataset entry is formatted as <dataset>/<perm1>,<perm2>,... and entries are separated by semicolons. The string representation of each Permission object is obtained via str(permission).

Notes

The function does not perform validation of the individual Permission objects beyond relying on their __str__ method. It is intended for serialising permission specifications for downstream processing or logging.

Example

from plantdb.commons.auth.session import dataset_perm_to_str from plantdb.commons.auth.models import Permission ds = {'dataset_A': [Permission.READ], 'dataset_B': [Permission.READ, Permission.CREATE]} dataset_perm_to_str(ds) 'dataset_A/Permission.READ;dataset_B/Permission.READ,Permission.CREATE'

Source code in plantdb/commons/auth/models.py
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
def dataset_perm_to_str(dataset_perm: Dict[str, list[Permission]]) -> str:
    """Convert a mapping of dataset names to permission lists into a compact string representation.

    Parameters
    ----------
    dataset_perm
        Mapping from dataset identifiers (strings) to a list of
        ``Permission`` objects that define the access rights for each dataset.

    Returns
    -------
    str
        A single string where each dataset entry is formatted as
        ``<dataset>/<perm1>,<perm2>,...`` and entries are separated by
        semicolons.  The string representation of each ``Permission`` object
        is obtained via ``str(permission)``.

    Notes
    -----
    The function does not perform validation of the individual ``Permission``
    objects beyond relying on their ``__str__`` method.  It is intended for
    serialising permission specifications for downstream processing or logging.

    Example
    -------
    >>> from plantdb.commons.auth.session import dataset_perm_to_str
    >>> from plantdb.commons.auth.models import Permission
    >>> ds = {'dataset_A': [Permission.READ], 'dataset_B': [Permission.READ, Permission.CREATE]}
    >>> dataset_perm_to_str(ds)
    'dataset_A/Permission.READ;dataset_B/Permission.READ,Permission.CREATE'
    """
    return ";".join([ds + '/' + ",".join(map(str, perms)) for ds, perms in dataset_perm.items()])

parse_dataset_perm Link

parse_dataset_perm(datasets_str)

Convert a dataset permission string back into the original mapping.

Parameters:

Name Type Description Default

datasets_str Link

str

The semicolon‑separated string.

required

Returns:

Type Description
Dict[str, set[Permission]]

Mapping of dataset names to a set of Permission. Empty permission lists are represented as an empty list.

Example

from plantdb.commons.auth.session import parse_dataset_perm s = 'dataset_A/Permission.READ;dataset_B/Permission.READ,Permission.CREATE' parse_dataset_perm(s) {'dataset_A': {}, 'dataset_B': {, }}

Source code in plantdb/commons/auth/models.py
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
def parse_dataset_perm(datasets_str: str) -> Dict[str, set[Permission]]:
    """Convert a dataset permission string back into the original mapping.

    Parameters
    ----------
    datasets_str:
        The semicolon‑separated string.

    Returns
    -------
    Dict[str, set[Permission]]
        Mapping of dataset names to a set of Permission.
        Empty permission lists are represented as an empty list.

    Example
    -------
    >>> from plantdb.commons.auth.session import parse_dataset_perm
    >>> s = 'dataset_A/Permission.READ;dataset_B/Permission.READ,Permission.CREATE'
    >>> parse_dataset_perm(s)
    {'dataset_A': {<Permission.READ: 'read'>}, 'dataset_B': {<Permission.CREATE: 'create'>, <Permission.READ: 'read'>}}
    """
    # Guard against an empty string.
    if not datasets_str:
        return {}

    result: Dict[str, set[Permission]] = {}

    # Split on the outer “;” separator - each piece corresponds to one dataset.
    for part in datasets_str.split(";"):
        # Each part is ``<dataset>/<perm1>,<perm2>,...``.
        # ``maxsplit=1`` protects us if a dataset name itself contains a '/'.
        if "/" not in part:
            # If there is no '/', treat the whole part as a dataset with no perms.
            ds, perms_section = part, ""
        else:
            ds, perms_section = part.split("/", 1)
        # An empty permissions section means the original list was empty.
        perms = set() if perms_section == "" else set(perms_section.split(","))
        result[ds] = {Permission.from_string(perm) for perm in perms}

    return result