Skip to content

Commit

Permalink
noop: start working on basic orm features
Browse files Browse the repository at this point in the history
  • Loading branch information
alexogeny committed Jul 31, 2024
1 parent 41840b2 commit ed12b5d
Show file tree
Hide file tree
Showing 5 changed files with 175 additions and 0 deletions.
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ dependencies = [
"click>=8.1.7",
"orjson>=3.10.6",
"uvicorn>=0.30.4",
"sqlalchemy>=2.0.31",
]
readme = "README.md"
requires-python = ">= 3.12"
Expand Down
6 changes: 6 additions & 0 deletions requirements-dev.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ cffi==1.16.0
click==8.1.7
# via uvicorn
# via zara
greenlet==3.0.3
# via sqlalchemy
h11==0.14.0
# via uvicorn
nodeenv==1.9.1
Expand All @@ -34,5 +36,9 @@ pycparser==2.22
pyflakes==3.2.0
pyright==1.1.374
ruff==0.5.5
sqlalchemy==2.0.31
# via zara
typing-extensions==4.12.2
# via sqlalchemy
uvicorn==0.30.4
# via zara
6 changes: 6 additions & 0 deletions requirements.lock
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,17 @@ cffi==1.16.0
click==8.1.7
# via uvicorn
# via zara
greenlet==3.0.3
# via sqlalchemy
h11==0.14.0
# via uvicorn
orjson==3.10.6
# via zara
pycparser==2.22
# via cffi
sqlalchemy==2.0.31
# via zara
typing-extensions==4.12.2
# via sqlalchemy
uvicorn==0.30.4
# via zara
162 changes: 162 additions & 0 deletions src/zara/database/mixins.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
from datetime import datetime
from typing import Any, Dict, List, Optional
from uuid import UUID

from argon2 import PasswordHasher
from sqlalchemy import Boolean, Column, DateTime, ForeignKey, Integer, String
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
from sqlalchemy.future import select
from sqlalchemy.orm import (
Mapped,
as_declarative,
declarative_base,
declarative_mixin,
declared_attr,
mapped_column,
relationship,
sessionmaker,
)

password_hasher = PasswordHasher()
DATABASE_URL = "sqlite+aiosqlite:///./test.db"

Base = declarative_base()

engine = create_async_engine(DATABASE_URL, echo=True)
AsyncSessionLocal = sessionmaker(
bind=engine, class_=AsyncSession, expire_on_commit=False
)


@as_declarative()
class CreateTableName:
@declared_attr
def __tablename__(cls):
return cls.__name__.lower().replace(" ", "_")


class PublicSchemaMixin:
@declared_attr
def __table_args__(cls):
return {"schema": "public"}


class CustomerSchemaMixin:
@declared_attr
def __table_args__(cls):
return {"schema": f"{cls.customer_name}"}


@declarative_mixin
class AuditMixin:
created_at = Column(DateTime, default=datetime.utcnow)
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
deleted_at = Column(DateTime, nullable=True)

created_by: Mapped[UUID] = mapped_column(ForeignKey("user.id"), nullable=True)
updated_by: Mapped[UUID] = mapped_column(ForeignKey("user.id"), nullable=True)
deleted_by: Mapped[UUID] = mapped_column(ForeignKey("user.id"), nullable=True)

created_by_user: Mapped["User"] = relationship(
back_populates="created_users", nullable=True
)
updated_by_user: Mapped["User"] = relationship(
back_populates="updated_users", nullable=True
)
deleted_by_user: Mapped["User"] = relationship(
back_populates="deleted_users", nullable=True
)


@declarative_mixin
class PasswordMixin:
@classmethod
def hash_password(
cls, password: str, app_salt: str, customer_salt: str, user_salt: str
) -> str:
salted_password = f"{app_salt}{customer_salt}{user_salt}{password}"
return password_hasher.hash(salted_password)

@classmethod
def verify_password(
cls,
hashed_password: str,
password: str,
app_salt: str,
customer_salt: str,
user_salt: str,
) -> bool:
salted_password = f"{app_salt}{customer_salt}{user_salt}{password}"
try:
return password_hasher.verify(hashed_password, salted_password)
except Exception:
return False

def set_password(
self, password: str, app_salt: str, customer_salt: str, user_salt: str
) -> None:
self.password = self.hash_password(password, app_salt, customer_salt, user_salt)


class BaseMixin(Base):
__abstract__ = True

id = Column(Integer, primary_key=True, index=True)

@declared_attr
def __tablename__(cls) -> str:
return cls.__name__.lower()

@classmethod
async def create(cls, **kwargs: Any) -> Any:
async with AsyncSessionLocal() as session:
obj = cls(**kwargs)
session.add(obj)
await session.commit()
await session.refresh(obj)
return obj

@classmethod
async def get(cls, model_id: int) -> Optional[Any]:
async with AsyncSessionLocal() as session:
result = await session.get(cls, model_id)
return result

@classmethod
async def update(cls, model_id: int, update_data: Dict[str, Any]) -> Optional[Any]:
async with AsyncSessionLocal() as session:
result = await session.get(cls, model_id)
if result:
for key, value in update_data.items():
setattr(result, key, value)
await session.commit()
await session.refresh(result)
return result

@classmethod
async def delete(cls, model_id: int) -> None:
async with AsyncSessionLocal() as session:
result = await session.get(cls, model_id)
if result:
await session.delete(result)
await session.commit()

@classmethod
async def list(cls) -> List[Any]:
async with AsyncSessionLocal() as session:
result = await session.execute(select(cls))
return result.scalars().all()


class Customer(BaseMixin, PublicSchemaMixin):
name = Column(String, unique=True, index=True)
display_name = Column(String)
customer_salt = Column(String)


class User(BaseMixin, CustomerSchemaMixin, AuditMixin, PasswordMixin):
username = Column(String, unique=True, index=True)
display_name = Column(String)
password = Column(String)
password_needs_update = Column(Boolean, default=False)
user_salt = Column(String)
Empty file added src/zara/database/table.py
Empty file.

0 comments on commit ed12b5d

Please sign in to comment.