python-general-be-lib 0.8.4__py3-none-any.whl → 0.9.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- general/exception/__init__.py +24 -0
- general/exception/access_exceptions.py +8 -9
- general/exception/crud_exceptions.py +34 -35
- general/exception/exception_interface.py +14 -21
- general/interface/__init__.py +8 -0
- general/interface/base/__init__.py +10 -0
- general/interface/base/base_model.py +28 -28
- general/interface/base/declarative_base.py +69 -62
- general/interface/metadata/__init__.py +12 -0
- general/interface/metadata/crud_metadata.py +193 -207
- general/interface/metadata/geom_metadata.py +92 -98
- general/interface/repository/__init__.py +9 -0
- general/interface/repository/crud_repository.py +337 -346
- general/interface/repository/geometry_repository.py +68 -92
- general/interface/repository/handler/__init__.py +20 -0
- general/interface/repository/handler/base_handler.py +29 -70
- general/interface/repository/handler/handler.py +72 -0
- general/interface/repository/handler/ilike_handler.py +40 -56
- general/interface/repository/handler/interval_handler.py +53 -90
- general/interface/repository/handler/json_handler.py +63 -51
- general/interface/repository/handler/json_ilike_handler.py +28 -53
- general/interface/repository/handler/or_handler.py +25 -58
- general/interface/repository/handler/statement_manager.py +80 -72
- general/interface/repository/many_to_many_repository.py +57 -74
- general/interface/repository/view_repository.py +81 -54
- general/logger.py +29 -65
- general/paginator_dto.py +21 -39
- general/py.typed +0 -0
- {python_general_be_lib-0.8.4.dist-info → python_general_be_lib-0.9.1.dist-info}/METADATA +13 -3
- python_general_be_lib-0.9.1.dist-info/RECORD +34 -0
- {python_general_be_lib-0.8.4.dist-info → python_general_be_lib-0.9.1.dist-info}/WHEEL +1 -1
- general/log_config.ini +0 -21
- python_general_be_lib-0.8.4.dist-info/RECORD +0 -32
- {python_general_be_lib-0.8.4.dist-info → python_general_be_lib-0.9.1.dist-info}/licenses/LICENSE +0 -0
- {python_general_be_lib-0.8.4.dist-info → python_general_be_lib-0.9.1.dist-info}/top_level.txt +0 -0
general/exception/__init__.py
CHANGED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""HTTP exceptions of the library.
|
|
2
|
+
|
|
3
|
+
They all extend ``ExceptionInterface``, which in turn extends
|
|
4
|
+
``fastapi.HTTPException``: raised inside an endpoint they are automatically
|
|
5
|
+
serialized into the response with their own status code. The mapping applied by
|
|
6
|
+
``CrudRepository._handle_exception`` to the SQLAlchemy errors is documented in
|
|
7
|
+
``crud_exceptions``.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .access_exceptions import *
|
|
11
|
+
from .crud_exceptions import *
|
|
12
|
+
from .exception_interface import *
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"ExceptionInterface",
|
|
16
|
+
"BadRequestException",
|
|
17
|
+
"EntityNotFoundException",
|
|
18
|
+
"ServiceUnavailableException",
|
|
19
|
+
"UnprocessableEntityException",
|
|
20
|
+
"HasNoAttributeException",
|
|
21
|
+
"InternalServerException",
|
|
22
|
+
"ForbiddenException",
|
|
23
|
+
"UnauthorizedException",
|
|
24
|
+
]
|
|
@@ -1,23 +1,22 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""HTTP exceptions for authentication and authorization.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
Il package ``general.exception`` non re-esporta questi simboli: vanno importati
|
|
8
|
-
dal modulo, ``from general.exception.access_exceptions import ForbiddenException``.
|
|
3
|
+
Unlike ``crud_exceptions``, these exceptions are never raised automatically by the
|
|
4
|
+
library: they are available to the authentication and authorization layers of the
|
|
5
|
+
projects using it.
|
|
9
6
|
"""
|
|
10
7
|
|
|
8
|
+
__all__ = ["ForbiddenException", "UnauthorizedException"]
|
|
9
|
+
|
|
11
10
|
from .exception_interface import ExceptionInterface
|
|
12
11
|
|
|
13
12
|
|
|
14
13
|
class ForbiddenException(ExceptionInterface):
|
|
15
|
-
"""403
|
|
14
|
+
"""403 - Authenticated user without the permissions for the requested resource."""
|
|
16
15
|
status_code = 403
|
|
17
16
|
default_message = "Risorsa non accessibile da questo utente"
|
|
18
17
|
|
|
19
18
|
|
|
20
19
|
class UnauthorizedException(ExceptionInterface):
|
|
21
|
-
"""401
|
|
20
|
+
"""401 - Request without valid credentials."""
|
|
22
21
|
status_code = 401
|
|
23
22
|
default_message = "Non autorizzato"
|
|
@@ -1,77 +1,77 @@
|
|
|
1
|
-
"""
|
|
1
|
+
"""HTTP exceptions for the persistence operations.
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- ``NoResultFound`` → ``EntityNotFoundException`` (404)
|
|
8
|
-
- ``DataError`` | ``IntegrityError`` → ``BadRequestException`` (400)
|
|
9
|
-
|
|
10
|
-
Il package ``general.exception`` non re-esporta questi simboli: vanno importati
|
|
11
|
-
dal modulo, ``from general.exception.crud_exceptions import BadRequestException``.
|
|
3
|
+
These are the exceptions ``CrudRepository._handle_exception`` maps the SQLAlchemy
|
|
4
|
+
errors onto: ``OperationalError`` to ``ServiceUnavailableException`` (503),
|
|
5
|
+
``NoResultFound`` to ``EntityNotFoundException`` (404), ``DataError`` and
|
|
6
|
+
``IntegrityError`` to ``BadRequestException`` (400).
|
|
12
7
|
"""
|
|
13
8
|
|
|
9
|
+
__all__ = [
|
|
10
|
+
"BadRequestException",
|
|
11
|
+
"EntityNotFoundException",
|
|
12
|
+
"ServiceUnavailableException",
|
|
13
|
+
"UnprocessableEntityException",
|
|
14
|
+
"HasNoAttributeException",
|
|
15
|
+
"InternalServerException",
|
|
16
|
+
]
|
|
17
|
+
|
|
14
18
|
from .exception_interface import ExceptionInterface
|
|
15
19
|
|
|
16
20
|
|
|
17
21
|
class BadRequestException(ExceptionInterface):
|
|
18
|
-
"""400
|
|
22
|
+
"""400 - Invalid request data or violated integrity constraint.
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
Raised in place of ``DataError`` and ``IntegrityError``, such as a foreign key,
|
|
25
|
+
unique or NOT NULL violation.
|
|
22
26
|
"""
|
|
23
27
|
status_code = 400
|
|
24
28
|
default_message = "Richiesta non corretta"
|
|
25
29
|
|
|
26
30
|
|
|
27
31
|
class EntityNotFoundException(ExceptionInterface):
|
|
28
|
-
"""404
|
|
32
|
+
"""404 - No record matches the requested criteria.
|
|
29
33
|
|
|
30
|
-
|
|
34
|
+
Raised in place of ``NoResultFound``.
|
|
31
35
|
"""
|
|
32
36
|
status_code = 404
|
|
33
37
|
default_message = "Dato non trovato"
|
|
34
38
|
|
|
35
39
|
|
|
36
40
|
class ServiceUnavailableException(ExceptionInterface):
|
|
37
|
-
"""503
|
|
41
|
+
"""503 - Database unreachable.
|
|
38
42
|
|
|
39
|
-
|
|
40
|
-
|
|
43
|
+
Raised in place of ``OperationalError``: dropped connection, exhausted pool, server
|
|
44
|
+
not listening.
|
|
41
45
|
"""
|
|
42
46
|
status_code = 503
|
|
43
47
|
default_message = "Servizio momentaneamente non disponibile, riprovare più tardi"
|
|
44
48
|
|
|
45
49
|
|
|
46
50
|
class UnprocessableEntityException(ExceptionInterface):
|
|
47
|
-
"""422
|
|
51
|
+
"""422 - Syntactically valid but unprocessable entity.
|
|
48
52
|
|
|
49
|
-
|
|
50
|
-
|
|
53
|
+
Never raised automatically by the repositories: it is available to the services for
|
|
54
|
+
their application validation errors.
|
|
51
55
|
"""
|
|
52
56
|
status_code = 422
|
|
53
57
|
default_message = "Entità non processabile"
|
|
54
58
|
|
|
55
59
|
|
|
56
60
|
class HasNoAttributeException(ExceptionInterface):
|
|
57
|
-
"""400
|
|
58
|
-
|
|
59
|
-
Sollevata quando un nome di colonna passato a ``columns``, ``exclude_columns``
|
|
60
|
-
o ``order_by`` non esiste sull'entità.
|
|
61
|
+
"""400 - Column or attribute missing on the entity.
|
|
61
62
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
# detail: "Richiesta non corretta - Attributo cognome non presente"
|
|
63
|
+
Raised when a column name passed to ``columns``, ``exclude_columns`` or ``order_by``
|
|
64
|
+
does not exist on the entity.
|
|
65
65
|
"""
|
|
66
66
|
status_code = 400
|
|
67
67
|
default_message = "Richiesta non corretta"
|
|
68
68
|
|
|
69
69
|
def __init__(self, attr: str = ""):
|
|
70
|
-
"""
|
|
70
|
+
"""Compose the detail according to the presence of the attribute name.
|
|
71
71
|
|
|
72
72
|
Args:
|
|
73
|
-
attr (str):
|
|
74
|
-
|
|
73
|
+
attr (str): Name of the missing attribute. When omitted, a generic message not
|
|
74
|
+
identifying the column is used.
|
|
75
75
|
"""
|
|
76
76
|
if attr:
|
|
77
77
|
super().__init__(custom_message=f"Attributo {attr} non presente", )
|
|
@@ -80,11 +80,10 @@ class HasNoAttributeException(ExceptionInterface):
|
|
|
80
80
|
|
|
81
81
|
|
|
82
82
|
class InternalServerException(ExceptionInterface):
|
|
83
|
-
"""500
|
|
83
|
+
"""500 - Unexpected internal error.
|
|
84
84
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
inesistente.
|
|
85
|
+
Used as a fallback by the ``@log`` decorator for every exception that is not already
|
|
86
|
+
an ``HTTPException``, and by ``CrudMetadata.get_table`` for a missing table.
|
|
88
87
|
"""
|
|
89
88
|
status_code = 500
|
|
90
89
|
default_message = "Impossibile gestire la richiesta al momento"
|
|
@@ -1,38 +1,31 @@
|
|
|
1
|
+
__all__ = ["ExceptionInterface"]
|
|
2
|
+
|
|
1
3
|
from fastapi import HTTPException
|
|
2
4
|
|
|
3
5
|
|
|
4
6
|
class ExceptionInterface(HTTPException):
|
|
5
|
-
"""
|
|
6
|
-
|
|
7
|
-
Estende ``fastapi.HTTPException`` spostando status code e messaggio su
|
|
8
|
-
**attributi di classe**: ogni sottoclasse dichiara il proprio ``status_code``
|
|
9
|
-
e ``default_message`` e può quindi essere sollevata senza argomenti.
|
|
7
|
+
"""Base interface for every exception of the library.
|
|
10
8
|
|
|
11
|
-
|
|
12
|
-
|
|
9
|
+
Extends ``fastapi.HTTPException`` moving status code and message to **class
|
|
10
|
+
attributes**: every subclass declares its own ``status_code`` and
|
|
11
|
+
``default_message`` and can therefore be raised without arguments. Raised inside a
|
|
12
|
+
FastAPI endpoint, it is automatically serialized into the error response with the
|
|
13
|
+
declared status code.
|
|
13
14
|
|
|
14
15
|
Attributes:
|
|
15
|
-
status_code (int):
|
|
16
|
-
default_message (str):
|
|
17
|
-
|
|
18
|
-
Example:
|
|
19
|
-
>>> class TeapotException(ExceptionInterface):
|
|
20
|
-
... status_code = 418
|
|
21
|
-
... default_message = "Sono una teiera"
|
|
22
|
-
>>> raise TeapotException("niente caffè")
|
|
23
|
-
# detail: "Sono una teiera - niente caffè"
|
|
16
|
+
status_code (int): HTTP status code of the response.
|
|
17
|
+
default_message (str): Default message used as ``detail``.
|
|
24
18
|
"""
|
|
25
19
|
status_code: int
|
|
26
20
|
default_message: str
|
|
27
21
|
|
|
28
22
|
def __init__(self, custom_message: str = None):
|
|
29
|
-
"""
|
|
23
|
+
"""Compose the ``detail`` from the default message and the custom one.
|
|
30
24
|
|
|
31
25
|
Args:
|
|
32
|
-
custom_message (str, optional):
|
|
33
|
-
``default_message``
|
|
34
|
-
|
|
35
|
-
è il solo ``default_message``.
|
|
26
|
+
custom_message (str, optional): Additional detail, appended to the
|
|
27
|
+
``default_message`` as ``"{default_message} - {custom_message}"``. When
|
|
28
|
+
omitted, the ``detail`` is the ``default_message`` alone.
|
|
36
29
|
"""
|
|
37
30
|
detail = f"{self.default_message} - {custom_message}" if custom_message else self.default_message
|
|
38
31
|
super().__init__(status_code=self.status_code, detail=detail)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"""ORM repositories, ORM-free metadata layer and shared base classes.
|
|
2
|
+
|
|
3
|
+
Container package: the symbols are imported from the subpackages
|
|
4
|
+
(``general.interface.base``, ``general.interface.repository``,
|
|
5
|
+
``general.interface.repository.handler``, ``general.interface.metadata``). It is
|
|
6
|
+
declared explicitly, instead of being left as an implicit namespace package, so as
|
|
7
|
+
not to depend on the PEP 420 support of the tools consuming the distribution.
|
|
8
|
+
"""
|
|
@@ -1,2 +1,12 @@
|
|
|
1
|
+
"""SQLAlchemy declarative base class and preconfigured Pydantic models."""
|
|
2
|
+
|
|
1
3
|
from .base_model import *
|
|
2
4
|
from .declarative_base import *
|
|
5
|
+
|
|
6
|
+
__all__ = [
|
|
7
|
+
"Base",
|
|
8
|
+
"retrieve_mapper_entity",
|
|
9
|
+
"retrieve_mapper_from_table",
|
|
10
|
+
"ExtBaseModel",
|
|
11
|
+
"CamelExtBaseModel",
|
|
12
|
+
]
|
|
@@ -8,66 +8,66 @@ from pydantic.alias_generators import to_camel
|
|
|
8
8
|
|
|
9
9
|
@lru_cache(maxsize=None)
|
|
10
10
|
def _alias_to_name(model: type[BaseModel]) -> dict[str, str]:
|
|
11
|
-
"""
|
|
11
|
+
"""Build the ``{alias: field_name}`` map of a model, once per class.
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
``model_fields``
|
|
13
|
+
Cached on the class, which is long-lived, to avoid the linear scan of
|
|
14
|
+
``model_fields`` on every ``get_attr_by_alias`` call.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
model (type[BaseModel]): Pydantic model to inspect.
|
|
18
|
+
|
|
19
|
+
Returns:
|
|
20
|
+
dict[str, str]: Map from alias to Python field name, limited to the fields
|
|
21
|
+
declaring an alias.
|
|
15
22
|
"""
|
|
16
23
|
return {field.alias: name for name, field in model.model_fields.items() if field.alias}
|
|
17
24
|
|
|
18
25
|
|
|
19
26
|
class ConfigModel(BaseModel):
|
|
20
|
-
"""
|
|
27
|
+
"""Base Pydantic model carrying the shared configuration.
|
|
21
28
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
- ``validate_default=True``: valida i valori di default alla creazione.
|
|
29
|
+
Enables ``from_attributes=True`` for the validation from SQLAlchemy ORM objects,
|
|
30
|
+
``populate_by_name=True`` to accept both the Python name and the alias,
|
|
31
|
+
``arbitrary_types_allowed=True`` to allow non-standard types such as ``WKBElement``,
|
|
32
|
+
and ``validate_default=True`` to validate the default values on creation.
|
|
27
33
|
"""
|
|
28
34
|
model_config = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
|
|
29
35
|
|
|
30
36
|
|
|
31
37
|
class CamelConfigModel(ConfigModel):
|
|
32
|
-
"""
|
|
38
|
+
"""Extend ``ConfigModel`` adding the automatic generation of camelCase aliases."""
|
|
33
39
|
model_config = ConfigDict(alias_generator=to_camel)
|
|
34
40
|
|
|
35
41
|
@classmethod
|
|
36
42
|
def get_attr_by_alias(cls, alias: str) -> str:
|
|
37
|
-
"""
|
|
43
|
+
"""Return the Python field name given a camelCase alias.
|
|
38
44
|
|
|
39
|
-
|
|
40
|
-
|
|
45
|
+
Useful to resolve the internal field name when receiving a payload with camelCase
|
|
46
|
+
keys.
|
|
41
47
|
|
|
42
48
|
Args:
|
|
43
|
-
alias (str):
|
|
49
|
+
alias (str): camelCase alias of the field, such as ``"myField"``.
|
|
44
50
|
|
|
45
51
|
Returns:
|
|
46
|
-
str:
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
Example:
|
|
50
|
-
>>> class MyModel(CamelExtBaseModel):
|
|
51
|
-
... my_field: str
|
|
52
|
-
>>> MyModel.get_attr_by_alias("myField")
|
|
53
|
-
'my_field'
|
|
52
|
+
str: Python name of the field, such as ``"my_field"``, or the alias itself when
|
|
53
|
+
not found.
|
|
54
54
|
"""
|
|
55
55
|
return _alias_to_name(cls).get(alias, alias)
|
|
56
56
|
|
|
57
57
|
|
|
58
58
|
class ExtBaseModel(ConfigModel):
|
|
59
|
-
"""
|
|
59
|
+
"""Pydantic model with the standard configuration for the ORM integration.
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
Inherits from ``ConfigModel``. Meant as the base of every model that has to be
|
|
62
|
+
validated from a SQLAlchemy entity.
|
|
63
63
|
"""
|
|
64
64
|
pass
|
|
65
65
|
|
|
66
66
|
|
|
67
67
|
class CamelExtBaseModel(CamelConfigModel):
|
|
68
|
-
"""
|
|
68
|
+
"""Pydantic model with automatic camelCase aliases and ORM integration.
|
|
69
69
|
|
|
70
|
-
|
|
71
|
-
|
|
70
|
+
Inherits from ``CamelConfigModel``. Meant for the cases where the API exposes
|
|
71
|
+
camelCase fields while the code works in snake_case.
|
|
72
72
|
"""
|
|
73
73
|
pass
|
|
@@ -1,46 +1,51 @@
|
|
|
1
1
|
__all__ = ["Base", "retrieve_mapper_entity", "retrieve_mapper_from_table"]
|
|
2
2
|
|
|
3
|
+
from functools import lru_cache
|
|
3
4
|
from typing import Optional, Any, Sequence
|
|
4
5
|
|
|
5
6
|
from geoalchemy2 import Geometry, WKBElement
|
|
6
|
-
from pydantic import create_model
|
|
7
|
+
from pydantic import create_model, ConfigDict
|
|
7
8
|
from sqlalchemy.inspection import inspect
|
|
8
9
|
from sqlalchemy.orm import Mapper, as_declarative
|
|
9
10
|
|
|
10
11
|
|
|
11
12
|
@as_declarative()
|
|
12
13
|
class Base:
|
|
13
|
-
"""
|
|
14
|
+
"""SQLAlchemy declarative base class enriched with utility methods.
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
Every ORM entity of the project has to extend this class. It gives quick access to
|
|
17
|
+
columns and primary keys, plus the conversion towards Pydantic.
|
|
17
18
|
"""
|
|
18
19
|
|
|
19
20
|
@classmethod
|
|
20
21
|
def to_pydantic(cls, model_name: str = None, full_optional: bool = False, default_values: dict[str, Any] = None, include_only: Sequence[str] = None, **create_model_kwargs):
|
|
21
|
-
"""
|
|
22
|
+
"""Generate a Pydantic model dynamically from the entity columns.
|
|
22
23
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
Iterates over the ORM-mapped columns and builds the model fields honouring the
|
|
25
|
+
nullability and the given default values; ``Geometry`` columns are mapped onto
|
|
26
|
+
``WKBElement``. When the caller passes neither ``__config__`` nor ``__base__``, the
|
|
27
|
+
generated model gets the same configuration as ``ExtBaseModel``. The default is all
|
|
28
|
+
or nothing: passing ``__config__`` puts the whole configuration in the hands of the
|
|
29
|
+
caller, and passing ``__base__`` lets the base carry its own.
|
|
26
30
|
|
|
27
31
|
Args:
|
|
28
|
-
model_name (str, optional):
|
|
29
|
-
|
|
30
|
-
full_optional (bool):
|
|
31
|
-
|
|
32
|
+
model_name (str, optional): Name of the generated model. When not given,
|
|
33
|
+
``{ClassName}Model`` is used.
|
|
34
|
+
full_optional (bool): If ``True``, every field becomes ``Optional`` with a
|
|
35
|
+
``None`` default, regardless of the nullability in the database.
|
|
32
36
|
Default: ``False``.
|
|
33
|
-
default_values (dict[str, Any], optional):
|
|
34
|
-
|
|
35
|
-
|
|
37
|
+
default_values (dict[str, Any], optional): ``{field_name: default_value}`` map
|
|
38
|
+
overriding the inferred defaults.
|
|
39
|
+
include_only (Sequence[str], optional): Attribute names to restrict the model to.
|
|
40
|
+
When ``None``, every mapped column is included.
|
|
41
|
+
**create_model_kwargs: Additional arguments passed to ``pydantic.create_model``.
|
|
36
42
|
|
|
37
43
|
Returns:
|
|
38
|
-
Type[BaseModel]:
|
|
39
|
-
|
|
40
|
-
Example:
|
|
41
|
-
>>> UserModel = User.to_pydantic(full_optional=True)
|
|
42
|
-
>>> PatchModel = User.to_pydantic(model_name="UserPatch", full_optional=True)
|
|
44
|
+
Type[BaseModel]: Dynamically generated Pydantic class.
|
|
43
45
|
"""
|
|
46
|
+
if "__config__" not in create_model_kwargs.keys() and "__base__" not in create_model_kwargs.keys():
|
|
47
|
+
create_model_kwargs["__config__"] = ConfigDict(from_attributes=True, populate_by_name=True, arbitrary_types_allowed=True, validate_default=True)
|
|
48
|
+
|
|
44
49
|
if not default_values:
|
|
45
50
|
default_values = dict()
|
|
46
51
|
mapper: Mapper = inspect(cls)
|
|
@@ -67,70 +72,76 @@ class Base:
|
|
|
67
72
|
|
|
68
73
|
@classmethod
|
|
69
74
|
def columns(cls):
|
|
70
|
-
"""
|
|
75
|
+
"""Return the column collection of the table the entity is mapped to.
|
|
71
76
|
|
|
72
77
|
Returns:
|
|
73
|
-
ReadOnlyColumnCollection:
|
|
74
|
-
|
|
75
|
-
Example:
|
|
76
|
-
>>> User.columns()
|
|
78
|
+
ReadOnlyColumnCollection: Collection of the SQLAlchemy columns.
|
|
77
79
|
"""
|
|
78
80
|
return cls.__table__.c
|
|
79
81
|
|
|
80
82
|
@classmethod
|
|
81
83
|
def columns_names(cls):
|
|
82
|
-
"""
|
|
84
|
+
"""Return the names of every column of the table.
|
|
83
85
|
|
|
84
86
|
Returns:
|
|
85
|
-
list[str]:
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
>>> User.columns_names()
|
|
89
|
-
['id', 'name', 'email']
|
|
87
|
+
list[str]: Names of the columns, in definition order. They are the **database**
|
|
88
|
+
names, which differ from the Python attribute names on the entities
|
|
89
|
+
declaring an explicit column name.
|
|
90
90
|
"""
|
|
91
91
|
return [col.name for col in cls.__table__.c]
|
|
92
92
|
|
|
93
93
|
@classmethod
|
|
94
94
|
def primary_key(cls):
|
|
95
|
-
"""
|
|
95
|
+
"""Return the columns making up the primary key of the table.
|
|
96
96
|
|
|
97
97
|
Returns:
|
|
98
|
-
list[Column]:
|
|
99
|
-
|
|
100
|
-
Example:
|
|
101
|
-
>>> User.primary_key()
|
|
102
|
-
[Column('id', Integer(), ...)]
|
|
98
|
+
list[Column]: Columns forming the primary key.
|
|
103
99
|
"""
|
|
104
100
|
return cls.__table__.primary_key.columns.values()
|
|
105
101
|
|
|
106
102
|
def to_dict(self):
|
|
107
|
-
"""
|
|
103
|
+
"""Convert the instance into an ``{attribute: value}`` dictionary.
|
|
108
104
|
|
|
109
|
-
|
|
110
|
-
|
|
105
|
+
The keys are the names of the **Python attributes** of the mapped columns, not the
|
|
106
|
+
names of the columns in the database; the two differ only on the entities declaring
|
|
107
|
+
an explicit column name. Relationships are not included, only columns.
|
|
111
108
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
{'id': 1, 'name': 'Mario', 'email': 'mario@example.com'}
|
|
109
|
+
Returns:
|
|
110
|
+
dict[str, Any]: Current values of the instance.
|
|
115
111
|
"""
|
|
116
|
-
return {key: getattr(self, key) for key in self.__class__
|
|
112
|
+
return {key: getattr(self, key) for key in _attributes_names(self.__class__)}
|
|
117
113
|
|
|
118
114
|
|
|
119
|
-
|
|
120
|
-
|
|
115
|
+
@lru_cache(maxsize=None)
|
|
116
|
+
def _attributes_names(entity: type) -> tuple[str, ...]:
|
|
117
|
+
"""Return the Python attribute names of the mapped columns, memoized per class.
|
|
121
118
|
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
The memoization is at module level rather than a class attribute: the cache key is
|
|
120
|
+
the class itself, so a subclass in single table inheritance gets its own tuple
|
|
121
|
+
instead of inheriting the one of its parent. The names are read from
|
|
122
|
+
``__mapper__.columns`` and not from ``column_attrs``, which would force
|
|
123
|
+
``configure_mappers()``.
|
|
124
124
|
|
|
125
125
|
Args:
|
|
126
|
-
|
|
126
|
+
entity (type): ORM class to inspect.
|
|
127
127
|
|
|
128
128
|
Returns:
|
|
129
|
-
|
|
129
|
+
tuple[str, ...]: Attribute names, in definition order.
|
|
130
|
+
"""
|
|
131
|
+
return tuple(entity.__mapper__.columns.keys())
|
|
130
132
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
133
|
+
|
|
134
|
+
def retrieve_mapper_entity(entity_name: str):
|
|
135
|
+
"""Look up a subclass of ``Base`` by class name.
|
|
136
|
+
|
|
137
|
+
Iterates over ``Base.__subclasses__()``, so it only finds the **direct** subclasses
|
|
138
|
+
of ``Base`` and not the ones nested in further inheritance levels.
|
|
139
|
+
|
|
140
|
+
Args:
|
|
141
|
+
entity_name (str): Name of the ORM class to look for, such as ``"User"``.
|
|
142
|
+
|
|
143
|
+
Returns:
|
|
144
|
+
Type[Base] | None: The matching ORM class, or ``None`` when not found.
|
|
134
145
|
"""
|
|
135
146
|
for mapper in Base.__subclasses__():
|
|
136
147
|
if mapper.__name__ == entity_name:
|
|
@@ -138,20 +149,16 @@ def retrieve_mapper_entity(entity_name: str):
|
|
|
138
149
|
|
|
139
150
|
|
|
140
151
|
def retrieve_mapper_from_table(table_name: str):
|
|
141
|
-
"""
|
|
152
|
+
"""Look up a subclass of ``Base`` by table name.
|
|
142
153
|
|
|
143
|
-
|
|
144
|
-
|
|
154
|
+
Iterates over ``Base.__subclasses__()``, so it only finds the **direct** subclasses
|
|
155
|
+
of ``Base`` and not the ones nested in further inheritance levels.
|
|
145
156
|
|
|
146
157
|
Args:
|
|
147
|
-
table_name (str):
|
|
158
|
+
table_name (str): Name of the table in the database, such as ``"users"``.
|
|
148
159
|
|
|
149
160
|
Returns:
|
|
150
|
-
Type[Base] | None:
|
|
151
|
-
|
|
152
|
-
Example:
|
|
153
|
-
>>> retrieve_mapper_from_table("users").__name__
|
|
154
|
-
'User'
|
|
161
|
+
Type[Base] | None: The matching ORM class, or ``None`` when not found.
|
|
155
162
|
"""
|
|
156
163
|
for mapper in Base.__subclasses__():
|
|
157
164
|
if mapper.__tablename__ == table_name:
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""CRUD and spatial layer over reflected ``Table`` objects, without the ORM.
|
|
2
|
+
|
|
3
|
+
Alternative to the repositories to operate on tables having no ORM model: the schema
|
|
4
|
+
is reflected at runtime and the tables are resolved by name. Unlike the
|
|
5
|
+
repositories, this layer does **not** use the handler pipeline and does **not** map
|
|
6
|
+
the SQLAlchemy exceptions onto HTTP exceptions.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from .crud_metadata import *
|
|
10
|
+
from .geom_metadata import *
|
|
11
|
+
|
|
12
|
+
__all__ = ["CrudMetadata", "GeomMetadata"]
|