2024-03-27 12:53:46 +00:00
|
|
|
import datetime
|
2024-04-06 23:21:55 +00:00
|
|
|
import inspect
|
2024-04-21 09:47:23 +00:00
|
|
|
import typing
|
2023-11-15 17:20:13 +00:00
|
|
|
from collections import ChainMap
|
2024-04-21 09:47:23 +00:00
|
|
|
from typing import Annotated
|
2024-04-17 07:03:54 +00:00
|
|
|
from typing import ClassVar
|
|
|
|
from typing import get_origin
|
|
|
|
from typing import get_type_hints
|
2023-11-15 17:20:13 +00:00
|
|
|
|
|
|
|
from canaille.app import classproperty
|
2024-04-06 23:21:55 +00:00
|
|
|
from canaille.app import models
|
2024-04-16 20:42:29 +00:00
|
|
|
from canaille.backends import Backend
|
2023-11-15 17:20:13 +00:00
|
|
|
|
|
|
|
|
2023-08-17 13:55:41 +00:00
|
|
|
class Model:
|
2024-03-29 08:35:29 +00:00
|
|
|
"""The model abstract class.
|
2023-08-17 13:55:41 +00:00
|
|
|
|
2024-04-01 16:25:38 +00:00
|
|
|
It details all the common attributes shared by every models.
|
2024-03-27 12:53:46 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
id: str | None = None
|
2024-04-05 14:05:17 +00:00
|
|
|
"""A unique identifier for a SCIM resource as defined by the service
|
|
|
|
provider. Id will be :py:data:`None` until the
|
|
|
|
:meth:`~canaille.backends.models.BackendModel.save` method is called.
|
|
|
|
|
|
|
|
Each representation of the resource MUST include a non-empty "id"
|
|
|
|
value. This identifier MUST be unique across the SCIM service
|
|
|
|
provider's entire set of resources. It MUST be a stable, non-
|
|
|
|
reassignable identifier that does not change when the same resource
|
|
|
|
is returned in subsequent requests. The value of the "id" attribute
|
|
|
|
is always issued by the service provider and MUST NOT be specified
|
|
|
|
by the client. The string "bulkId" is a reserved keyword and MUST
|
|
|
|
NOT be used within any unique identifier value. The attribute
|
|
|
|
characteristics are "caseExact" as "true", a mutability of
|
|
|
|
"readOnly", and a "returned" characteristic of "always". See
|
|
|
|
Section 9 for additional considerations regarding privacy.
|
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
created: datetime.datetime | None = None
|
2024-03-29 08:35:29 +00:00
|
|
|
"""The :class:`~datetime.datetime` that the resource was added to the
|
|
|
|
service provider."""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
last_modified: datetime.datetime | None = None
|
2024-03-29 08:35:29 +00:00
|
|
|
"""The most recent :class:`~datetime.datetime` that the details of this
|
|
|
|
resource were updated at the service provider.
|
2024-03-27 12:53:46 +00:00
|
|
|
|
|
|
|
If this resource has never been modified since its initial creation,
|
2024-03-29 08:35:29 +00:00
|
|
|
the value MUST be the same as the value of :attr:`~canaille.backends.models.Model.created`.
|
2024-03-27 12:53:46 +00:00
|
|
|
"""
|
|
|
|
|
2024-10-28 08:13:00 +00:00
|
|
|
_attributes: ClassVar[list[str] | None] = None
|
2024-04-07 17:22:54 +00:00
|
|
|
|
2024-04-06 20:46:11 +00:00
|
|
|
@classproperty
|
|
|
|
def attributes(cls):
|
2024-04-07 17:22:54 +00:00
|
|
|
if not cls._attributes:
|
|
|
|
annotations = ChainMap(
|
|
|
|
*(
|
2024-04-21 09:47:23 +00:00
|
|
|
get_type_hints(klass, include_extras=True)
|
2024-04-07 17:22:54 +00:00
|
|
|
for klass in reversed(cls.__mro__)
|
|
|
|
if issubclass(klass, Model)
|
|
|
|
)
|
2024-04-06 20:46:11 +00:00
|
|
|
)
|
2024-04-17 07:03:54 +00:00
|
|
|
# only keep types that are not ClassVar
|
2024-04-07 17:22:54 +00:00
|
|
|
cls._attributes = {
|
|
|
|
key: value
|
|
|
|
for key, value in annotations.items()
|
2024-04-17 07:03:54 +00:00
|
|
|
if get_origin(value) is not ClassVar
|
2024-04-07 17:22:54 +00:00
|
|
|
}
|
|
|
|
return cls._attributes
|
2024-04-06 20:46:11 +00:00
|
|
|
|
2024-04-16 20:02:47 +00:00
|
|
|
def __html__(self):
|
|
|
|
return self.id
|
|
|
|
|
2024-04-22 18:04:24 +00:00
|
|
|
@property
|
|
|
|
def identifier(self):
|
|
|
|
"""Returns a unique value that will be used to identify the model
|
|
|
|
instance.
|
|
|
|
|
|
|
|
This value will be used in URLs in canaille, so it should be
|
|
|
|
unique and short.
|
|
|
|
"""
|
|
|
|
return getattr(self, self.identifier_attribute)
|
|
|
|
|
2024-04-01 16:25:38 +00:00
|
|
|
|
|
|
|
class BackendModel:
|
|
|
|
"""The backend model abstract class.
|
|
|
|
|
|
|
|
It details all the methods and attributes that are expected to be
|
|
|
|
implemented for every model and for every backend.
|
|
|
|
"""
|
|
|
|
|
2024-04-06 23:21:55 +00:00
|
|
|
@classmethod
|
2024-04-21 09:47:23 +00:00
|
|
|
def get_model_annotations(cls, attribute):
|
|
|
|
annotations = cls.attributes[attribute]
|
|
|
|
|
|
|
|
# Extract the list type from list annotations
|
|
|
|
attribute_type = (
|
|
|
|
typing.get_args(annotations)[0]
|
|
|
|
if typing.get_origin(annotations) is list
|
|
|
|
else annotations
|
2024-04-06 23:21:55 +00:00
|
|
|
)
|
2024-04-21 09:47:23 +00:00
|
|
|
|
|
|
|
# Extract the Annotated annotation
|
|
|
|
attribute_type, metadata = (
|
|
|
|
typing.get_args(attribute_type)
|
|
|
|
if typing.get_origin(attribute_type) == Annotated
|
|
|
|
else (attribute_type, None)
|
2024-04-06 23:21:55 +00:00
|
|
|
)
|
|
|
|
|
2024-04-21 09:47:23 +00:00
|
|
|
if not inspect.isclass(attribute_type) or not issubclass(attribute_type, Model):
|
|
|
|
return None, None
|
|
|
|
|
|
|
|
if not metadata:
|
|
|
|
return attribute_type, None
|
|
|
|
|
|
|
|
return attribute_type, metadata.get("backref")
|
|
|
|
|
2024-04-06 23:21:55 +00:00
|
|
|
def match_filter(self, filter):
|
|
|
|
if filter is None:
|
|
|
|
return True
|
|
|
|
|
|
|
|
if isinstance(filter, list):
|
|
|
|
return any(self.match_filter(subfilter) for subfilter in filter)
|
|
|
|
|
|
|
|
# If attribute are models, resolve the instance
|
2024-11-06 14:00:54 +00:00
|
|
|
filter = filter.copy()
|
2024-04-06 23:21:55 +00:00
|
|
|
for attribute, value in filter.items():
|
2024-04-21 09:47:23 +00:00
|
|
|
model, _ = self.get_model_annotations(attribute)
|
2024-04-06 23:21:55 +00:00
|
|
|
|
2024-04-21 09:47:23 +00:00
|
|
|
if not model or isinstance(value, Model):
|
2024-04-06 23:21:55 +00:00
|
|
|
continue
|
|
|
|
|
2024-04-21 09:47:23 +00:00
|
|
|
backend_model = getattr(models, model.__name__)
|
2024-04-06 23:21:55 +00:00
|
|
|
|
2024-04-16 20:42:29 +00:00
|
|
|
if instance := Backend.instance.get(backend_model, value):
|
2024-04-06 23:21:55 +00:00
|
|
|
filter[attribute] = instance
|
|
|
|
|
|
|
|
return all(
|
|
|
|
getattr(self, attribute) and value in getattr(self, attribute)
|
|
|
|
for attribute, value in filter.items()
|
|
|
|
)
|