Skip to content

model

Model implementation for a User.

RESOURCE_ALL_VALUES = '*' module-attribute

Special character used to identify all values of a certain permission.

BasicUser

Bases: BaseModel

A model class representing a user of the system

Source code in mlte/user/model.py
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
class BasicUser(BaseModel):
    """A model class representing a user of the system"""

    username: str
    """The username to uniquely identify a user."""

    email: Optional[str] = None
    """An optional email associated to the user."""

    full_name: Optional[str] = None
    """The full name of the user."""

    disabled: bool = False
    """Whether the user is disabled."""

    role: RoleType = RoleType.REGULAR
    """The role associated to the user."""

    groups: list[Group] = []
    """The groups the user is in."""

    def is_equal_to(
        self,
        user: BasicUser,
        only_group_names: bool = True,
        ignore_groups: bool = False,
    ) -> bool:
        """Compares users at the BasicUser level."""
        user1 = BasicUser(**self.to_json())
        user2 = BasicUser(**user.to_json())

        if only_group_names:
            user1.groups = Group.get_group_names(user1.groups)
            user2.groups = Group.get_group_names(user2.groups)

        if ignore_groups:
            user1.groups = []
            user2.groups = []

        return user1 == user2

    def __eq__(self, other: object) -> bool:
        """Compares users, taking into account that groups may be in different orders."""
        if not isinstance(other, BasicUser):
            return False

        # Check general fields.
        if (
            self.username != other.username
            or self.email != other.email
            or self.full_name != other.full_name
            or self.disabled != other.disabled
            or self.role != other.role
        ):
            return False

        # Compare the groups list sorted to ignore order.
        key_func = lambda x: tuple(x.model_dump().items())  # noqa
        return sorted(self.groups, key=key_func) == sorted(
            other.groups, key=key_func
        )

disabled = False class-attribute instance-attribute

Whether the user is disabled.

email = None class-attribute instance-attribute

An optional email associated to the user.

full_name = None class-attribute instance-attribute

The full name of the user.

groups = [] class-attribute instance-attribute

The groups the user is in.

role = RoleType.REGULAR class-attribute instance-attribute

The role associated to the user.

username instance-attribute

The username to uniquely identify a user.

__eq__(other)

Compares users, taking into account that groups may be in different orders.

Source code in mlte/user/model.py
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
def __eq__(self, other: object) -> bool:
    """Compares users, taking into account that groups may be in different orders."""
    if not isinstance(other, BasicUser):
        return False

    # Check general fields.
    if (
        self.username != other.username
        or self.email != other.email
        or self.full_name != other.full_name
        or self.disabled != other.disabled
        or self.role != other.role
    ):
        return False

    # Compare the groups list sorted to ignore order.
    key_func = lambda x: tuple(x.model_dump().items())  # noqa
    return sorted(self.groups, key=key_func) == sorted(
        other.groups, key=key_func
    )

is_equal_to(user, only_group_names=True, ignore_groups=False)

Compares users at the BasicUser level.

Source code in mlte/user/model.py
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
def is_equal_to(
    self,
    user: BasicUser,
    only_group_names: bool = True,
    ignore_groups: bool = False,
) -> bool:
    """Compares users at the BasicUser level."""
    user1 = BasicUser(**self.to_json())
    user2 = BasicUser(**user.to_json())

    if only_group_names:
        user1.groups = Group.get_group_names(user1.groups)
        user2.groups = Group.get_group_names(user2.groups)

    if ignore_groups:
        user1.groups = []
        user2.groups = []

    return user1 == user2

Group

Bases: BaseModel

A user group to which permissions are associated.

Source code in mlte/user/model.py
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
class Group(BaseModel):
    """A user group to which permissions are associated."""

    name: str
    """The name of the group."""

    permissions: list[Permission] = []
    """The permissions associated to the group."""

    @staticmethod
    def get_group_names(groups: list[Group]) -> list[Group]:
        """Given a list of groups, returns a similar list with groups that only contain their names."""
        group_names: list[Group] = []
        for group in groups:
            group_names.append(Group(name=group.name))
        return group_names

name instance-attribute

The name of the group.

permissions = [] class-attribute instance-attribute

The permissions associated to the group.

get_group_names(groups) staticmethod

Given a list of groups, returns a similar list with groups that only contain their names.

Source code in mlte/user/model.py
197
198
199
200
201
202
203
@staticmethod
def get_group_names(groups: list[Group]) -> list[Group]:
    """Given a list of groups, returns a similar list with groups that only contain their names."""
    group_names: list[Group] = []
    for group in groups:
        group_names.append(Group(name=group.name))
    return group_names

MethodType

Bases: StrEnum

Types of methods for permissions.

Source code in mlte/user/model.py
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
class MethodType(StrEnum):
    """Types of methods for permissions."""

    GET = "get"
    """Get or read action."""

    POST = "post"
    """Creation action."""

    PUT = "put"
    """Action to edit."""

    DELETE = "delete"
    """Deletion action."""

    ANY = "any"
    """Special action to represent all/any of them."""

ANY = 'any' class-attribute instance-attribute

Special action to represent all/any of them.

DELETE = 'delete' class-attribute instance-attribute

Deletion action.

GET = 'get' class-attribute instance-attribute

Get or read action.

POST = 'post' class-attribute instance-attribute

Creation action.

PUT = 'put' class-attribute instance-attribute

Action to edit.

Permission

Bases: BaseModel

Permissions for manipulating resources.

Source code in mlte/user/model.py
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
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
class Permission(BaseModel):
    """Permissions for manipulating resources."""

    SEPARATOR: ClassVar[str] = "-"
    SEPARATOR_REPLACEMENT: ClassVar[str] = "___"
    """Used when serializing to string."""

    resource_type: ResourceType
    """The type of resource resource."""

    resource_id: Optional[str] = None
    """The specific resource id to give permissions to, if any."""

    method: MethodType = MethodType.ANY
    """The HTTP method applied on the resource."""

    def to_str(self) -> str:
        """Serialize the permission to a string"""
        serialized = f"{self.resource_type}{Permission.SEPARATOR}{self.method}"
        if self.resource_id is not None:
            serialized = f"{serialized}{Permission.SEPARATOR}{self.resource_id.replace(Permission.SEPARATOR, Permission.SEPARATOR_REPLACEMENT)}"
        return serialized

    @staticmethod
    def from_str(permission_str: str) -> Permission:
        """Creates a permission from its string serialization."""
        # Source str can have 2 or 3 parts, depending if resource id was None.
        parts = permission_str.split(Permission.SEPARATOR)
        type = parts[0]
        method = parts[1]
        if len(parts) > 2:
            resource_id = parts[2].replace(
                Permission.SEPARATOR_REPLACEMENT, Permission.SEPARATOR
            )
        else:
            resource_id = None

        return Permission(
            resource_type=ResourceType(type),
            resource_id=resource_id,
            method=MethodType(method),
        )

    def grants_access(self, request: Permission):
        """Checks if this permission grants access to the recieved request."""
        if self.to_str() == request.to_str():
            # If both are exactly the same, they match.
            return True

        if self.resource_type != request.resource_type:
            # If they point to different resource types, they will never match.
            return False
        else:
            if (
                self.method == request.method
                or self.method == MethodType.ANY
                or request.method == MethodType.ANY
            ):
                # If methods match, or either refer to any method, check resource id.
                if self.resource_id is None:
                    # "None" means all, that means we apply to any request resource id.
                    return True
                elif self.resource_id == request.resource_id:
                    # If the resource ids are the same, we match.
                    return True
                else:
                    # If we have a specific resource id, and they have a different one or "all", we don't match.
                    return False
            else:
                # The methods don't match in some way.
                return False

SEPARATOR_REPLACEMENT = '___' class-attribute

Used when serializing to string.

method = MethodType.ANY class-attribute instance-attribute

The HTTP method applied on the resource.

resource_id = None class-attribute instance-attribute

The specific resource id to give permissions to, if any.

resource_type instance-attribute

The type of resource resource.

from_str(permission_str) staticmethod

Creates a permission from its string serialization.

Source code in mlte/user/model.py
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
@staticmethod
def from_str(permission_str: str) -> Permission:
    """Creates a permission from its string serialization."""
    # Source str can have 2 or 3 parts, depending if resource id was None.
    parts = permission_str.split(Permission.SEPARATOR)
    type = parts[0]
    method = parts[1]
    if len(parts) > 2:
        resource_id = parts[2].replace(
            Permission.SEPARATOR_REPLACEMENT, Permission.SEPARATOR
        )
    else:
        resource_id = None

    return Permission(
        resource_type=ResourceType(type),
        resource_id=resource_id,
        method=MethodType(method),
    )

grants_access(request)

Checks if this permission grants access to the recieved request.

Source code in mlte/user/model.py
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
def grants_access(self, request: Permission):
    """Checks if this permission grants access to the recieved request."""
    if self.to_str() == request.to_str():
        # If both are exactly the same, they match.
        return True

    if self.resource_type != request.resource_type:
        # If they point to different resource types, they will never match.
        return False
    else:
        if (
            self.method == request.method
            or self.method == MethodType.ANY
            or request.method == MethodType.ANY
        ):
            # If methods match, or either refer to any method, check resource id.
            if self.resource_id is None:
                # "None" means all, that means we apply to any request resource id.
                return True
            elif self.resource_id == request.resource_id:
                # If the resource ids are the same, we match.
                return True
            else:
                # If we have a specific resource id, and they have a different one or "all", we don't match.
                return False
        else:
            # The methods don't match in some way.
            return False

to_str()

Serialize the permission to a string

Source code in mlte/user/model.py
222
223
224
225
226
227
def to_str(self) -> str:
    """Serialize the permission to a string"""
    serialized = f"{self.resource_type}{Permission.SEPARATOR}{self.method}"
    if self.resource_id is not None:
        serialized = f"{serialized}{Permission.SEPARATOR}{self.resource_id.replace(Permission.SEPARATOR, Permission.SEPARATOR_REPLACEMENT)}"
    return serialized

ResourceType

Bases: StrEnum

Supported resource types.

Source code in mlte/user/model.py
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
class ResourceType(StrEnum):
    """Supported resource types."""

    MODEL = "model"
    """Model and all related artifacts."""

    USER = "user"
    """User resources."""

    GROUP = "group"
    """Group resources."""

    CATALOG = "catalog"
    """Test catalogs."""

    CUSTOM_LIST = "custom_list"
    """Custom lists."""

    @staticmethod
    def get_type_from_url(url: str) -> Optional[ResourceType]:
        """Returns the resource type for the given URL."""
        for resource_type in ResourceType:
            if url.startswith(f"/{resource_type.value}"):
                return resource_type

        # Return none if the URL did not match any known resource type.
        return None

CATALOG = 'catalog' class-attribute instance-attribute

Test catalogs.

CUSTOM_LIST = 'custom_list' class-attribute instance-attribute

Custom lists.

GROUP = 'group' class-attribute instance-attribute

Group resources.

MODEL = 'model' class-attribute instance-attribute

Model and all related artifacts.

USER = 'user' class-attribute instance-attribute

User resources.

get_type_from_url(url) staticmethod

Returns the resource type for the given URL.

Source code in mlte/user/model.py
177
178
179
180
181
182
183
184
185
@staticmethod
def get_type_from_url(url: str) -> Optional[ResourceType]:
    """Returns the resource type for the given URL."""
    for resource_type in ResourceType:
        if url.startswith(f"/{resource_type.value}"):
            return resource_type

    # Return none if the URL did not match any known resource type.
    return None

RoleType

Bases: StrEnum

Roles for users.

Source code in mlte/user/model.py
20
21
22
23
24
25
26
27
class RoleType(StrEnum):
    """Roles for users."""

    ADMIN = "admin"
    """An admin role, access to everything."""

    REGULAR = "regular"
    """A role with access to artifacts only."""

ADMIN = 'admin' class-attribute instance-attribute

An admin role, access to everything.

REGULAR = 'regular' class-attribute instance-attribute

A role with access to artifacts only.

User

Bases: BasicUser

User with all needed information, and no plain password.

Source code in mlte/user/model.py
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
class User(BasicUser):
    """User with all needed information, and no plain password."""

    hashed_password: str
    """The hashed password of the user."""

    def update_user_data(self, new_user_data: BasicUser) -> User:
        """Update this user, but keeping existing hashed password."""
        hashed_password = self.hashed_password
        updated_user = User(
            **new_user_data.to_json(),
            hashed_password=hashed_password,
        )
        return updated_user

hashed_password instance-attribute

The hashed password of the user.

update_user_data(new_user_data)

Update this user, but keeping existing hashed password.

Source code in mlte/user/model.py
 99
100
101
102
103
104
105
106
def update_user_data(self, new_user_data: BasicUser) -> User:
    """Update this user, but keeping existing hashed password."""
    hashed_password = self.hashed_password
    updated_user = User(
        **new_user_data.to_json(),
        hashed_password=hashed_password,
    )
    return updated_user

UserWithPassword

Bases: BasicUser

User with additional information only used when creating a user.

Source code in mlte/user/model.py
109
110
111
112
113
114
115
116
117
118
119
120
class UserWithPassword(BasicUser):
    """User with additional information only used when creating a user."""

    password: str
    """The plain password of the user."""

    def to_hashed_user(self) -> User:
        """Converts a UserWithPassword model with plain password into a User with a hashed one."""
        # Hash password and create a user with hashed passwords.
        hashed_password = passwords.hash_password(self.password)
        user = User(hashed_password=hashed_password, **self.to_json())
        return user

password instance-attribute

The plain password of the user.

to_hashed_user()

Converts a UserWithPassword model with plain password into a User with a hashed one.

Source code in mlte/user/model.py
115
116
117
118
119
120
def to_hashed_user(self) -> User:
    """Converts a UserWithPassword model with plain password into a User with a hashed one."""
    # Hash password and create a user with hashed passwords.
    hashed_password = passwords.hash_password(self.password)
    user = User(hashed_password=hashed_password, **self.to_json())
    return user

update_user_data(curr_user, new_user_data)

Get updated user depending on the type, keeping hashed password if no new password is received.

Source code in mlte/user/model.py
123
124
125
126
127
128
129
130
131
132
def update_user_data(
    curr_user: User, new_user_data: Union[UserWithPassword, BasicUser]
) -> User:
    """Get updated user depending on the type, keeping hashed password if no new password is received."""
    if type(new_user_data) is UserWithPassword:
        return new_user_data.to_hashed_user()
    elif type(new_user_data) is BasicUser:
        return curr_user.update_user_data(new_user_data)
    else:
        raise Exception(f"Invalid user type received: {type(new_user_data)}")