generated from daniil-berg/boilerplate-py
Add first couple of ORM models; implement some helper functions
This commit is contained in:
parent
71f9db6c0d
commit
b7e627a216
129
src/compub/db/companies.py
Normal file
129
src/compub/db/companies.py
Normal file
@ -0,0 +1,129 @@
|
||||
from datetime import date
|
||||
from typing import Optional
|
||||
from uuid import uuid4
|
||||
|
||||
from slugify import slugify
|
||||
from sqlalchemy.engine import Connection
|
||||
from sqlalchemy.event.api import listens_for
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.orm.mapper import Mapper
|
||||
from sqlalchemy.sql.expression import select
|
||||
from sqlalchemy.sql.functions import count
|
||||
from sqlalchemy.sql.schema import Column, ForeignKey as FKey, Table
|
||||
from sqlalchemy.sql.sqltypes import Boolean, Date, Integer, String, Unicode
|
||||
from sqlalchemy_utils.types import CountryType, UUIDType
|
||||
|
||||
from compub.utils import multi_max
|
||||
from .base import AbstractBase, ORMBase
|
||||
|
||||
|
||||
class LegalForm(AbstractBase):
|
||||
__tablename__ = 'legal_form'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
short = Column(String(32), nullable=False, index=True)
|
||||
name = Column(Unicode(255))
|
||||
country = Column(CountryType)
|
||||
|
||||
subcategories = relationship('LegalFormSubcategory', backref='legal_form', lazy='selectin')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.short)
|
||||
|
||||
|
||||
class LegalFormSubcategory(AbstractBase):
|
||||
__tablename__ = 'legal_form_subcategory'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
legal_form_id = Column(Integer, FKey('legal_form.id', ondelete='RESTRICT'), nullable=False, index=True)
|
||||
short = Column(String(32), nullable=False, index=True)
|
||||
name = Column(Unicode(255))
|
||||
|
||||
companies = relationship('Company', backref='legal_form', lazy='selectin')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.short)
|
||||
|
||||
|
||||
class Industry(AbstractBase):
|
||||
__tablename__ = 'industry'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
name = Column(String(255), nullable=False, index=True)
|
||||
|
||||
companies = relationship('Company', secondary='company_industries', back_populates='industries')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
|
||||
class Company(AbstractBase):
|
||||
__tablename__ = 'company'
|
||||
|
||||
NAME_SORTING_PARAMS = ( # passed to `multi_max`
|
||||
lambda obj: date(1, 1, 1) if obj.date_registered is None else obj.date_registered,
|
||||
'date_updated',
|
||||
)
|
||||
# If date_registered is None, the name is considered to be older than one with a date_registered
|
||||
|
||||
id = Column(UUIDType, primary_key=True, default=uuid4)
|
||||
visible = Column(Boolean, default=True, nullable=False, index=True)
|
||||
legal_form_id = Column(Integer, FKey('legal_form_subcategory.id', ondelete='RESTRICT'), index=True)
|
||||
insolvent = Column(Boolean, default=False, nullable=False, index=True)
|
||||
founding_date = Column(Date)
|
||||
liquidation_date = Column(Date)
|
||||
# TODO: Get rid of city; implement address properly
|
||||
city = Column(String(255), index=True)
|
||||
address_id = Column(UUIDType, FKey('address.id', ondelete='RESTRICT'), index=True)
|
||||
|
||||
industries = relationship('Industry', secondary='company_industries', back_populates='companies')
|
||||
names = relationship('CompanyName', backref='company', lazy='selectin')
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.current_name or f"<Company {self.id}>")
|
||||
|
||||
@property
|
||||
def current_name(self) -> Optional['CompanyName']:
|
||||
return multi_max(list(self.names), *self.NAME_SORTING_PARAMS, default=None)
|
||||
|
||||
|
||||
company_industries = Table(
|
||||
'company_industries',
|
||||
ORMBase.metadata,
|
||||
Column('company_id', FKey('company.id'), primary_key=True),
|
||||
Column('industry_id', FKey('industry.id'), primary_key=True),
|
||||
)
|
||||
|
||||
|
||||
class CompanyName(AbstractBase):
|
||||
__tablename__ = 'company_name'
|
||||
|
||||
MAX_SLUG_LENGTH = 255
|
||||
|
||||
id = Column(UUIDType, primary_key=True, default=uuid4)
|
||||
name = Column(Unicode(768), nullable=False, index=True)
|
||||
company_id = Column(UUIDType, FKey('company.id', ondelete='RESTRICT'), index=True)
|
||||
date_registered = Column(Date)
|
||||
slug = Column(String(MAX_SLUG_LENGTH), index=True)
|
||||
|
||||
def __str__(self) -> str:
|
||||
return str(self.name)
|
||||
|
||||
|
||||
@listens_for(CompanyName, 'before_insert')
|
||||
def generate_company_name_slug(_mapper: Mapper, connection: Connection, target: CompanyName) -> None:
|
||||
if target.slug:
|
||||
return
|
||||
slug = slugify(target.name)[:(target.MAX_SLUG_LENGTH - 2)]
|
||||
statement = select(count()).select_from(CompanyName).where(CompanyName.slug.startswith(slug))
|
||||
num = connection.execute(statement).scalar()
|
||||
if num == 0:
|
||||
target.slug = slug
|
||||
else:
|
||||
target.slug = f'{slug}-{str(num + 1)}'
|
||||
|
||||
|
||||
def get_reg_date(obj: CompanyName) -> date:
|
||||
if obj.date_registered is None:
|
||||
return date(1, 1, 1)
|
||||
return obj.date_registered
|
48
src/compub/db/geography.py
Normal file
48
src/compub/db/geography.py
Normal file
@ -0,0 +1,48 @@
|
||||
from uuid import uuid4
|
||||
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.sql.schema import Column, ForeignKey as FKey
|
||||
from sqlalchemy.sql.sqltypes import Integer, String, Unicode
|
||||
from sqlalchemy_utils.types import CountryType, UUIDType
|
||||
|
||||
from .base import AbstractBase
|
||||
|
||||
|
||||
class StateProvince(AbstractBase):
|
||||
__tablename__ = 'state_province'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
country = Column(CountryType, nullable=False, index=True)
|
||||
name = Column(Unicode(255), nullable=False, index=True)
|
||||
|
||||
cities = relationship('City', backref='state_province', lazy='selectin')
|
||||
|
||||
|
||||
class City(AbstractBase):
|
||||
__tablename__ = 'city'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
state_province_id = Column(Integer, FKey('state_province.id', ondelete='RESTRICT'), nullable=False, index=True)
|
||||
zip_code = Column(String(5), nullable=False, index=True)
|
||||
name = Column(Unicode(255), nullable=False, index=True)
|
||||
|
||||
streets = relationship('Street', backref='city', lazy='selectin')
|
||||
|
||||
|
||||
class Street(AbstractBase):
|
||||
__tablename__ = 'street'
|
||||
|
||||
id = Column(Integer, primary_key=True)
|
||||
city_id = Column(Integer, FKey('city.id', ondelete='RESTRICT'), nullable=False, index=True)
|
||||
name = Column(Unicode(255), nullable=False, index=True)
|
||||
|
||||
addresses = relationship('Address', backref='street', lazy='selectin')
|
||||
|
||||
|
||||
class Address(AbstractBase):
|
||||
__tablename__ = 'address'
|
||||
|
||||
id = Column(UUIDType, primary_key=True, default=uuid4)
|
||||
street_id = Column(Integer, FKey('street.id', ondelete='RESTRICT'), nullable=False, index=True)
|
||||
house_number = Column(String(8), nullable=False)
|
||||
supplement = Column(String(255))
|
66
src/compub/utils.py
Normal file
66
src/compub/utils.py
Normal file
@ -0,0 +1,66 @@
|
||||
from operator import attrgetter
|
||||
from typing import Any, Callable, TypeVar
|
||||
|
||||
|
||||
T = TypeVar('T')
|
||||
KeyFuncT = Callable[[T], Any]
|
||||
_sentinel = object()
|
||||
|
||||
|
||||
def multi_sort(obj_list: list[T], *parameters: str | KeyFuncT | tuple[str | KeyFuncT, bool]) -> None:
|
||||
for param in reversed(parameters):
|
||||
if isinstance(param, str):
|
||||
obj_list.sort(key=attrgetter(param))
|
||||
elif callable(param):
|
||||
obj_list.sort(key=param)
|
||||
else:
|
||||
try:
|
||||
param, reverse = param
|
||||
assert isinstance(reverse, bool)
|
||||
except (ValueError, TypeError):
|
||||
raise ValueError(f"Sorting parameter {param} is neither a key nor a key-boolean-tuple.")
|
||||
if isinstance(param, str):
|
||||
obj_list.sort(key=attrgetter(param), reverse=reverse)
|
||||
elif callable(param):
|
||||
obj_list.sort(key=param, reverse=reverse)
|
||||
else:
|
||||
raise ValueError(f"Sorting key {param} is neither a string nor a callable.")
|
||||
|
||||
|
||||
def multi_gt(left: T, right: T, *parameters: str | KeyFuncT | tuple[str | KeyFuncT, bool]) -> bool:
|
||||
for param in parameters:
|
||||
invert = False
|
||||
if isinstance(param, str):
|
||||
left_val, right_val = getattr(left, param), getattr(right, param)
|
||||
elif callable(param):
|
||||
left_val, right_val = param(left), param(right)
|
||||
else:
|
||||
try:
|
||||
param, invert = param
|
||||
assert isinstance(invert, bool)
|
||||
except (ValueError, TypeError, AssertionError):
|
||||
raise ValueError(f"Ordering parameter {param} is neither a key nor a key-boolean-tuple.")
|
||||
if isinstance(param, str):
|
||||
left_val, right_val = getattr(left, param), getattr(right, param)
|
||||
elif callable(param):
|
||||
left_val, right_val = param(left), param(right)
|
||||
else:
|
||||
raise ValueError(f"Ordering key {param} is neither a string nor a callable.")
|
||||
if left_val == right_val:
|
||||
continue
|
||||
return left_val < right_val if invert else left_val > right_val
|
||||
return False
|
||||
|
||||
|
||||
def multi_max(obj_list: list[T], *parameters: str | KeyFuncT | tuple[str | KeyFuncT, bool],
|
||||
default: Any = _sentinel) -> T:
|
||||
try:
|
||||
largest = obj_list[0]
|
||||
except IndexError:
|
||||
if default is not _sentinel:
|
||||
return default
|
||||
raise ValueError("Cannot get largest item from an empty list.")
|
||||
for obj in obj_list[1:]:
|
||||
if multi_gt(obj, largest, *parameters):
|
||||
largest = obj
|
||||
return largest
|
Loading…
Reference in New Issue
Block a user