Implement BaseModel class, TimestampMixin, TzDateTime type; cleanup Task model
This commit is contained in:
parent
7ce43a2bfe
commit
252c15acbd
15 changed files with 223 additions and 42 deletions
|
|
@ -1 +1,3 @@
|
|||
from .metadata import MetaData
|
||||
from .sqlalchemy import SQLAlchemy
|
||||
from .typing import Col, Rel
|
||||
|
|
|
|||
25
tofu_api/common/database/metadata.py
Normal file
25
tofu_api/common/database/metadata.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
from sqlalchemy import MetaData as _MetaData
|
||||
|
||||
__all__ = [
|
||||
'MetaData',
|
||||
]
|
||||
|
||||
|
||||
class MetaData(_MetaData):
|
||||
"""
|
||||
App specific subclass of the SQLAlchemy MetaData class.
|
||||
"""
|
||||
|
||||
# Define naming convention for constraints
|
||||
_naming_convention = {
|
||||
"ix": 'ix_%(column_0_label)s',
|
||||
"uq": "uq_%(table_name)s_%(column_0_name)s",
|
||||
"ck": "ck_%(table_name)s_%(constraint_name)s",
|
||||
"fk": "fk_%(table_name)s_%(column_0_name)s_%(referred_table_name)s",
|
||||
"pk": "pk_%(table_name)s"
|
||||
}
|
||||
|
||||
def __init__(self, *args, naming_convention=None, **kwargs):
|
||||
if not naming_convention:
|
||||
naming_convention = self._naming_convention
|
||||
super().__init__(*args, naming_convention=naming_convention, **kwargs)
|
||||
23
tofu_api/common/database/mixins.py
Normal file
23
tofu_api/common/database/mixins.py
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, func
|
||||
from sqlalchemy.orm import declarative_mixin
|
||||
|
||||
from tofu_api.common.database import Col
|
||||
from tofu_api.common.database.types import TzDateTime
|
||||
|
||||
__all__ = [
|
||||
'TimestampMixin'
|
||||
]
|
||||
|
||||
|
||||
@declarative_mixin
|
||||
class TimestampMixin:
|
||||
"""
|
||||
Mixin for database models that provides the "created_at" and "modified_at" columns.
|
||||
"""
|
||||
# Created timestamp (automatically set to NOW() once on object creation)
|
||||
created_at: Col[datetime] = Column(TzDateTime, nullable=False, server_default=func.now())
|
||||
|
||||
# Modified timestamp (automatically set to NOW() on each update)
|
||||
modified_at: Col[datetime] = Column(TzDateTime, nullable=False, server_default=func.now(), onupdate=func.now())
|
||||
1
tofu_api/common/database/types/__init__.py
Normal file
1
tofu_api/common/database/types/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .tz_date_time import TzDateTime
|
||||
39
tofu_api/common/database/types/tz_date_time.py
Normal file
39
tofu_api/common/database/types/tz_date_time.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
from datetime import datetime, timezone
|
||||
|
||||
from sqlalchemy import DateTime, TypeDecorator
|
||||
|
||||
__all__ = [
|
||||
'TzDateTime',
|
||||
]
|
||||
|
||||
|
||||
class TzDateTime(TypeDecorator):
|
||||
"""
|
||||
Custom SQLAlchemy data type for timezone aware datetimes.
|
||||
"""
|
||||
impl = DateTime
|
||||
cache_ok = True
|
||||
|
||||
@property
|
||||
def python_type(self):
|
||||
return datetime
|
||||
|
||||
def process_bind_param(self, value: datetime, dialect):
|
||||
"""
|
||||
Convert a datetime object that is bound to a query parameter.
|
||||
"""
|
||||
if value is not None and value.tzinfo:
|
||||
value = value.astimezone(timezone.utc).replace(tzinfo=None)
|
||||
return value
|
||||
|
||||
def process_result_value(self, value: datetime, dialect):
|
||||
"""
|
||||
Convert a datetime object from a query result.
|
||||
"""
|
||||
return value.replace(tzinfo=timezone.utc) if value is not None else None
|
||||
|
||||
def process_literal_param(self, value: datetime, dialect):
|
||||
"""
|
||||
Convert a literal parameter value to be rendered inline within a statement.
|
||||
"""
|
||||
return self.process_bind_param(value, dialect)
|
||||
14
tofu_api/common/database/typing.py
Normal file
14
tofu_api/common/database/typing.py
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
from typing import TypeVar, Union
|
||||
|
||||
from sqlalchemy import Column
|
||||
from sqlalchemy.orm import RelationshipProperty
|
||||
|
||||
__all__ = [
|
||||
'Col',
|
||||
'Rel',
|
||||
]
|
||||
|
||||
# Define type aliases for SQLAlchemy columns and relationships in declarative models
|
||||
_T = TypeVar('_T')
|
||||
Col = Union[Column, _T]
|
||||
Rel = Union[RelationshipProperty, _T]
|
||||
1
tofu_api/common/json/__init__.py
Normal file
1
tofu_api/common/json/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
from .json_encoder import JSONEncoder
|
||||
29
tofu_api/common/json/json_encoder.py
Normal file
29
tofu_api/common/json/json_encoder.py
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from flask.json import JSONEncoder as _FlaskJSONEncoder
|
||||
|
||||
__all__ = [
|
||||
'JSONEncoder',
|
||||
]
|
||||
|
||||
|
||||
class JSONEncoder(_FlaskJSONEncoder):
|
||||
"""
|
||||
Custom JSON encoder built on top of the Flask JSONEncoder class.
|
||||
"""
|
||||
|
||||
def default(self, obj: Any) -> Any:
|
||||
"""
|
||||
Convert any object to a JSON serializable type.
|
||||
"""
|
||||
# Convert datetimes to ISO format without microseconds (e.g. '2022-01-02T10:20:30+00:00')
|
||||
if isinstance(obj, datetime):
|
||||
return obj.isoformat(timespec='seconds')
|
||||
|
||||
# Use to_dict() method on objects that have it
|
||||
if hasattr(obj, 'to_dict'):
|
||||
return obj.to_dict()
|
||||
|
||||
# Fallback to the Flask JSONEncoder
|
||||
return super().default(obj)
|
||||
Loading…
Add table
Add a link
Reference in a new issue