Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@ APP_NAME=ticket-api
APP_VERSION=0.1.0
DEBUG=false
API_V1_PREFIX=/api/v1
DATABASE_URL=postgresql+psycopg://ticket_user:ticket_password@localhost:5432/ticket_api
3 changes: 3 additions & 0 deletions app/core/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ class Settings(BaseSettings):
app_version: str = "0.1.0"
debug: bool = False
api_v1_prefix: str = "/api/v1"
database_url: str = (
"postgresql+psycopg://ticket_user:ticket_password@localhost:5432/ticket_api"
)


settings = Settings()
Empty file added app/db/__init__.py
Empty file.
5 changes: 5 additions & 0 deletions app/db/base.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from sqlalchemy.orm import DeclarativeBase


class Base(DeclarativeBase):
pass
27 changes: 27 additions & 0 deletions app/db/session.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
from collections.abc import Generator

from sqlalchemy import create_engine
from sqlalchemy.orm import Session, sessionmaker

from app.core.config import settings


engine = create_engine(
settings.database_url,
pool_pre_ping=True,
)

SessionLocal = sessionmaker(
autocommit=False,
autoflush=False,
bind=engine,
)


def get_db() -> Generator[Session, None, None]:
db = SessionLocal()

try:
yield db
finally:
db.close()
5 changes: 5 additions & 0 deletions app/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from app.models.incident import Incident
from app.models.ticket import Ticket
from app.models.user import User

__all__ = ["Incident", "Ticket", "User"]
41 changes: 41 additions & 0 deletions app/models/incident.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING

from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.db.base import Base

if TYPE_CHECKING:
from app.models.user import User


class Incident(Base):
__tablename__ = "incidents"

id: Mapped[int] = mapped_column(primary_key=True, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)

severity: Mapped[str] = mapped_column(String(50), default="medium", nullable=False)
status: Mapped[str] = mapped_column(String(50), default="open", nullable=False)

reported_by_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)

created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)

updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
onupdate=func.now(),
nullable=True,
)

reporter: Mapped["User"] = relationship(
back_populates="reported_incidents",
)
48 changes: 48 additions & 0 deletions app/models/ticket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING

from sqlalchemy import DateTime, ForeignKey, String, Text, func
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.db.base import Base

if TYPE_CHECKING:
from app.models.user import User


class Ticket(Base):
__tablename__ = "tickets"

id: Mapped[int] = mapped_column(primary_key=True, index=True)
title: Mapped[str] = mapped_column(String(200), nullable=False)
description: Mapped[str | None] = mapped_column(Text, nullable=True)

status: Mapped[str] = mapped_column(String(50), default="open", nullable=False)
priority: Mapped[str] = mapped_column(String(50), default="medium", nullable=False)

requester_id: Mapped[int] = mapped_column(ForeignKey("users.id"), nullable=False)
assignee_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True)

created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)

updated_at: Mapped[datetime | None] = mapped_column(
DateTime(timezone=True),
onupdate=func.now(),
nullable=True,
)

requester: Mapped["User"] = relationship(
back_populates="requested_tickets",
foreign_keys=[requester_id],
)

assignee: Mapped["User | None"] = relationship(
back_populates="assigned_tickets",
foreign_keys=[assignee_id],
)
45 changes: 45 additions & 0 deletions app/models/user.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from __future__ import annotations

from datetime import datetime
from typing import TYPE_CHECKING

from sqlalchemy import Boolean, DateTime, String, func
from sqlalchemy.orm import Mapped, mapped_column, relationship

from app.db.base import Base

if TYPE_CHECKING:
from app.models.incident import Incident
from app.models.ticket import Ticket


class User(Base):
__tablename__ = "users"

id: Mapped[int] = mapped_column(primary_key=True, index=True)
email: Mapped[str] = mapped_column(String(255), unique=True, index=True, nullable=False)
full_name: Mapped[str] = mapped_column(String(255), nullable=False)
hashed_password: Mapped[str] = mapped_column(String(255), nullable=False)

role: Mapped[str] = mapped_column(String(50), default="requester", nullable=False)
is_active: Mapped[bool] = mapped_column(Boolean, default=True, nullable=False)

created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
server_default=func.now(),
nullable=False,
)

requested_tickets: Mapped[list["Ticket"]] = relationship(
back_populates="requester",
foreign_keys="Ticket.requester_id",
)

assigned_tickets: Mapped[list["Ticket"]] = relationship(
back_populates="assignee",
foreign_keys="Ticket.assignee_id",
)

reported_incidents: Mapped[list["Incident"]] = relationship(
back_populates="reporter",
)
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ dependencies = [
"pydantic>=2.9.0,<3.0.0",
"pydantic-settings>=2.5.0,<3.0.0",
"uvicorn[standard]>=0.30.0,<1.0.0",
"sqlalchemy>=2.0.0,<3.0.0",
"psycopg[binary]>=3.2.0,<4.0.0",
]

[build-system]
Expand Down
10 changes: 10 additions & 0 deletions tests/test_database_config.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
from app.core.config import settings
from app.db.session import engine


def test_database_url_uses_postgresql_psycopg_driver() -> None:
assert settings.database_url.startswith("postgresql+psycopg://")


def test_engine_uses_postgresql_psycopg_driver() -> None:
assert engine.url.drivername == "postgresql+psycopg"
8 changes: 8 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
from app.db.base import Base
import app.models # noqa: F401


def test_database_models_are_registered():
expected_tables = {"users", "tickets", "incidents"}

assert expected_tables.issubset(Base.metadata.tables.keys())
Loading