Compare commits

..

3 Commits

Author SHA1 Message Date
Bernhard Radermacher (hakisto)
eb1d8d793c wip 2025-08-31 18:42:46 +02:00
Bernhard Radermacher (hakisto)
bd510016de wip 2025-08-31 07:53:17 +02:00
Bernhard Radermacher (hakisto)
5614ecbba6 contact working 2025-08-30 10:06:47 +02:00
15 changed files with 913 additions and 70 deletions

View File

@@ -18,8 +18,7 @@ def bidirectional_relationship(cls, foreign_table_cls):
column_name,
relationship(cls,
back_populates=foreign_table_cls.__tablename__,
cascade="all, delete-orphan",
collection_class=set,
cascade="all, delete-orphan"
)
)

View File

@@ -12,7 +12,7 @@ __all__ = ["Contact"]
class Contact(StatusForeignKey, Versioned, Base):
"""Contact"""
name: Mapped[str] = mapped_column(String(80), unique=True)
code: Mapped[str] = mapped_column(String(80), unique=True)
address: Mapped[str | None] = mapped_column(String(253))
notes: Mapped[str | None] = mapped_column(Text)

View File

@@ -1,3 +1,5 @@
from pydoc import describe
from sqlalchemy import String, Text, ForeignKey, event, select, UniqueConstraint
from sqlalchemy.orm import Mapped, mapped_column, declared_attr, Session
@@ -13,10 +15,12 @@ __all__ = ["Country", "LocationCode", "Location"]
class Country(StatusForeignKey, Versioned, Base):
iso: Mapped[str] = mapped_column(String(2), unique=True)
code: Mapped[str] = mapped_column(String(2), unique=True)
name: Mapped[str] = mapped_column(String(80))
notes: Mapped[str | None] = mapped_column(Text)
def __repr__(self):
return f"Country(id={self.id!r}, code={self.code!r}, name={self.name!r}, notes={self.notes!r}, location_codes={self.location_codes!r})"
class CountryForeignKey:
@@ -32,14 +36,14 @@ class CountryForeignKey:
@event.listens_for(Country.__table__, "after_create")
def initialize_country(target, connection, **kwargs):
with Session(connection) as session:
qsys = session.scalar(select(User).where(User.username == "QSYS"))
qsys = session.scalar(select(User).where(User.code == "QSYS"))
for kwargs in (
dict(iso="DE", name="Germany", status_id='A'),
dict(iso="IT", name="Italy", status_id='A'),
dict(iso="US", name="United States"),
dict(iso="CA", name="Canada"),
dict(iso="MX", name="Mexico"),
dict(iso="ES", name="Spain", status_id='A'),
dict(code="DE", name="Germany", status_id='A'),
dict(code="IT", name="Italy", status_id='A'),
dict(code="US", name="United States"),
dict(code="CA", name="Canada"),
dict(code="MX", name="Mexico"),
dict(code="ES", name="Spain", status_id='A'),
):
kwargs['_user__'] = qsys
session.add(Country(**kwargs))
@@ -50,9 +54,12 @@ class LocationCode(CountryForeignKey, ContactForeignKey, StatusForeignKey, Versi
"""Location Code"""
code: Mapped[str] = mapped_column(String(8), unique=True)
description: Mapped[str] = mapped_column(String(256))
description: Mapped[str | None] = mapped_column(String(80))
notes: Mapped[str | None] = mapped_column(Text)
def __repr__(self):
return f"LocationCode(id={self.id!r}, code={self.code!r}, description={self.description!r}, notes={self.notes!r}, locations={self.locations!r})"
class LocationCodeForeignKey:
"""Foreign Key Mixin for :py:class:`LocationCode`"""
@@ -65,17 +72,35 @@ class LocationCodeForeignKey:
return bidirectional_relationship(cls, LocationCode)
# noinspection PyUnusedLocal
@event.listens_for(LocationCode.__table__, "after_create")
def initialize_location_code(target, connection, **kwargs):
with Session(connection) as session:
qsys = session.scalar(select(User).where(User.code == "QSYS"))
de = session.scalar(select(Country).where(Country.code == "DE"))
for kwargs in (
dict(country=de, code="DEHAM", description="Hamburg", status_id='A'),
dict(country=de, code="DEFRA", description="Frankfurt/Main", status_id='A'),
):
kwargs['_user__'] = qsys
session.add(LocationCode(**kwargs))
session.commit()
class Location(LocationCodeForeignKey, ContactForeignKey, StatusForeignKey, Versioned, Base):
"""Location"""
location: Mapped[str] = mapped_column(String(30))
description: Mapped[str] = mapped_column(String(256))
code: Mapped[str] = mapped_column(String(30))
description: Mapped[str | None] = mapped_column(String(80))
notes: Mapped[str | None] = mapped_column(Text)
__table_args__ = (
UniqueConstraint('location_code_id', location),
UniqueConstraint('location_code_id', code),
)
def __repr__(self):
return f"Location(id={self.id!r}, code={self.code!r}, description={self.description!r}, notes={self.notes!r})"
class LocationForeignKey:
"""Foreign Key Mixin for :py:class:`Location`"""
@@ -86,3 +111,17 @@ class LocationForeignKey:
@declared_attr
def location(cls) -> Mapped["Location"]:
return bidirectional_relationship(cls, Location)
# noinspection PyUnusedLocal
@event.listens_for(Location.__table__, "after_create")
def initialize_location(target, connection, **kwargs):
with Session(connection) as session:
qsys = session.scalar(select(User).where(User.code == "QSYS"))
deham = session.scalar(select(LocationCode).where(LocationCode.code == "DEHAM"))
for kwargs in (
dict(location_code=deham, code="Andy's Wohnung"),
):
kwargs['_user__'] = qsys
session.add(Location(**kwargs))
session.commit()

View File

@@ -53,7 +53,7 @@ class PrinterModelForeignKey:
class Printer(PrinterModelForeignKey, LocationForeignKey, ContactForeignKey, StatusForeignKey, Versioned, Base):
"""Printer"""
name: Mapped[str] = mapped_column(String(63), unique=True)
code: Mapped[str] = mapped_column(String(63), unique=True)
description: Mapped[str] = mapped_column(String(256))
notes: Mapped[str | None] = mapped_column(Text)
dns_name: Mapped[str | None] = mapped_column(String(253))

View File

@@ -68,29 +68,29 @@ class Versioned:
class User(StatusForeignKey, Versioned, Base):
"""User"""
username: Mapped[str] = mapped_column(String(253), unique=True)
code: Mapped[str] = mapped_column(String(253), unique=True)
name: Mapped[str] = mapped_column(String(253))
password: Mapped[str | None] = mapped_column(String(255))
ldap_name: Mapped[str | None] = mapped_column(String(255))
notes: Mapped[str | None] = mapped_column(Text)
def __repr__(self):
return f'User(id={self.id!r}, username={self.username!r} name={self.name!r}, notes={self.notes!r})'
return f'User(id={self.id!r}, code={self.code!r} name={self.name!r}, notes={self.notes!r})'
# noinspection PyUnusedLocal
@event.listens_for(User.__table__, "after_create")
def initialize_user(target, connection, **kwargs):
from routers.user import get_password_hash
with Session(connection) as session:
qsys = User(username="QSYS", name="System User", notes="internal processing", status_id='X')
qsys = User(code="QSYS", name="System User", notes="internal processing", status_id='X')
session.add(qsys)
session.commit()
qsys = session.scalar(select(User).where(User.username == "QSYS"))
qsys = session.scalar(select(User).where(User.code == "QSYS"))
qsys._user__id=qsys.id
session.commit()
for kwargs in (
dict(username="CTM", name="Control-M", password=get_password_hash("secret"), notes="user for automation"),
dict(username="exde37c8", name="Bernhard Radermacher",
dict(code="CTM", name="Control-M", password=get_password_hash("secret"), notes="user for automation"),
dict(code="exde37c8", name="Bernhard Radermacher",
password=get_password_hash("secret"),
ldap_name="a0061806@kiongroup.com",
),

View File

@@ -7,14 +7,10 @@ from .engine import engine
from .session import get_session
from routers.user import get_current_active_user
ACTIVE_USER = Annotated[alchemy.User, Depends(get_current_active_user)]
# from routers.user import get_current_active_user
__all__ = [
'engine',
'ACTIVE_USER',
'get_session',
]

View File

@@ -1,6 +1,8 @@
import inspect
from contextlib import asynccontextmanager
from typing import Annotated
import fastapi
from fastapi.security import OAuth2PasswordRequestForm
from app.alchemy import Base
@@ -19,6 +21,11 @@ app = FastAPI(lifespan=lifespan)
app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"])
app.include_router(routers.status)
app.include_router(routers.user)
# app.include_router(routers.contact)
# app.include_router(routers.country)
# app.include_router(routers.status)
# app.include_router(routers.user)
for i in inspect.getmembers(routers):
if isinstance(i[1], fastapi.routing.APIRouter):
app.include_router(i[1])

View File

@@ -1,2 +1,6 @@
from .contact import router as contact
# from .country import router as country
# from .location_code import router as location_code
from .status import router as status
from .user import router as user
from .location import router as location

126
app/routers/contact.py Normal file
View File

@@ -0,0 +1,126 @@
import sys
from typing import Annotated
from fastapi import APIRouter, Query, Depends
from sqlmodel import SQLModel, Field
from sqlmodel import select
import alchemy
import utils
from dependencies import get_session
from utils import update_item, create_item
from .status_model import Status
from .user import ACTIVE_USER
PRIMARY_ANNOTATION = utils.make_primary_annotation('Contact')
# ----------------------------------------------------------------
# Models
class ContactBase(SQLModel):
code: str = Field(max_length=80, unique=True)
address: str = Field(max_length=253)
notes: str | None
class Contact(ContactBase):
id: int | None = Field(default=None, primary_key=True)
class ContactCreate(ContactBase):
address: str | None = None
notes: str | None = None
class ContactPublic(ContactBase):
id: int
status: Status
class ContactUpdate(ContactBase):
code: str | None = None
address: str | None = None
notes: str | None = None
# ----------------------------------------------------------------
# Routes
router = APIRouter(prefix="/contact", tags=["contact"])
@router.get("/", response_model=list[ContactPublic])
async def get_contacts(
offset: int = 0,
limit: Annotated[int, Query] = 100,
session=Depends(get_session)):
"""Get list of all contacts"""
if limit < 1:
limit = sys.maxsize
return session.exec(select(alchemy.Contact).offset(offset).limit(limit)).all()
@router.get("/{contact_id}",
response_model=ContactPublic,
responses={404: {"description": "Not found"}})
async def get_contact(
contact_id: PRIMARY_ANNOTATION,
session=Depends(get_session)):
return utils.get_single_record(session, alchemy.Contact, contact_id)
@router.post("/",
response_model=ContactPublic)
async def create_contact(
contact: ContactCreate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return create_item(
session=session,
cls=alchemy.Contact,
current_user=current_user,
data=Contact.model_validate(contact))
@router.patch("/{contact_id}",
response_model=ContactPublic,
responses={404: {"description": "Not found"}})
async def update_contact(
contact_id: PRIMARY_ANNOTATION,
contact: ContactUpdate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return update_item(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Contact, contact_id),
data=contact)
@router.put("/{contact_id}/activate",
response_model=ContactPublic,
responses={404: {"description": "Not found"}})
async def activate_contact(
contact_id: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Contact, contact_id),
status='A')
@router.put("/{contact_id}/deactivate",
response_model=ContactPublic,
responses={404: {"description": "Not found"}})
async def deactivate_contact(
contact_id: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Contact, contact_id),
status='I')

125
app/routers/country.py Normal file
View File

@@ -0,0 +1,125 @@
import sys
from typing import Annotated
from fastapi import APIRouter, Query, Depends
from sqlmodel import SQLModel, Field
from sqlmodel import select
import alchemy
import utils
from dependencies import get_session
from utils import update_item, create_item
from .status_model import Status
from .user import ACTIVE_USER
PRIMARY_ANNOTATION = utils.make_primary_annotation('Country')
# ----------------------------------------------------------------
# Models
class CountryBase(SQLModel):
code: str = Field(max_length=2, unique=True)
name: str = Field(max_length=80)
notes: str | None
class Country(CountryBase):
id: int | None = Field(default=None, primary_key=True)
class CountryCreate(CountryBase):
notes: str | None = None
class CountryPublic(CountryBase):
id: int
status: Status
class CountryUpdate(CountryBase):
code: str | None = None
name: str | None = None
notes: str | None = None
# ----------------------------------------------------------------
# Routes
router = APIRouter(prefix="/country", tags=["country"])
@router.get("/", response_model=list[CountryPublic])
async def get_countries(
offset: int = 0,
limit: Annotated[int, Query] = 100,
session=Depends(get_session)):
"""Get list of all countries"""
if limit < 1:
limit = sys.maxsize
return session.exec(select(alchemy.Country).offset(offset).limit(limit)).all()
@router.get("/{country_id}",
response_model=CountryPublic,
responses={404: {"description": "Not found"}})
async def get_country(
country_id: PRIMARY_ANNOTATION,
session=Depends(get_session)):
return utils.get_single_record(session, alchemy.Country, country_id)
@router.post("/",
response_model=CountryPublic)
async def create_country(
country: CountryCreate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return create_item(
session=session,
cls=alchemy.Country,
current_user=current_user,
data=Country.model_validate(country))
@router.patch("/{country_id}",
response_model=CountryPublic,
responses={404: {"description": "Not found"}})
async def update_country(
country_id: PRIMARY_ANNOTATION,
country: CountryUpdate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return update_item(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, country_id),
data=country)
@router.put("/{country_id}/activate",
response_model=CountryPublic,
responses={404: {"description": "Not found"}})
async def activate_country(
country_id: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, country_id),
status='A')
@router.put("/{country_id}/deactivate",
response_model=CountryPublic,
responses={404: {"description": "Not found"}})
async def deactivate_country(
country_id: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, country_id),
status='I')

307
app/routers/location.py Normal file
View File

@@ -0,0 +1,307 @@
import sys
from typing import Annotated
from fastapi import APIRouter, Query, Depends, HTTPException
# from sqlmodel import SQLModel, Field
from sqlmodel import select, and_
from pydantic import BaseModel, Field
# from sqlalchemy import select
import alchemy
import utils
from dependencies import get_session
from utils import update_item, create_item
from .status_model import Status
from .user import ACTIVE_USER
PRIMARY_ANNOTATION = utils.make_primary_annotation('Country')
# ----------------------------------------------------------------
# Models
# class LocationBase(SQLModel):
# code: str = Field(max_length=30)
# description: str | None = Field(max_length=256)
# notes: str | None
#
#
# class LocationCodeBase(SQLModel):
# code: str = Field(max_length=8)
# description: str | None= Field(max_length=256)
# notes: str | None
#
#
# class CountryBase(SQLModel):
# code: str = Field(max_length=2)
# name: str = Field(max_length=80)
# notes: str | None
# class ILocationCreate(LocationBase):
# description: str | None = None
# notes: str | None = None
#
#
# class ILocationCodeCreate(LocationCode):
# description: str | None = None
# notes: str | None = None
# class LocationCreate(LocationBase):
# description: str | None = None
# notes: str | None
#
#
# class LocationCodeCreate(LocationCodeBase):
# description: str | None = None
# notes: str | None = None
# locations: list[LocationCreate] | None = None
#
#
# class CountryCreate(CountryBase):
# notes: str | None = None
# location_codes: list[LocationCodeCreate] | None = None
# class Country(CountryBase):
# id: int | None = Field(default=None, primary_key=True)
#
#
# class CountryCreate(CountryBase):
# notes: str | None = None
class Contact(BaseModel):
code: str
address: str | None
notes: str | None
class Location(BaseModel):
id: int
code: str
description: str | None
notes: str | None
status: Status
contact: Contact | None
class LocationCode(BaseModel):
id: int
code: str
description: str | None
notes: str | None
status: Status
locations: list[Location]
contact: Contact | None
class Country(BaseModel):
id: int
code: str
name: str
notes: str | None
status: Status
location_codes: list[LocationCode] | None
class LocationCreate(BaseModel):
code: str = Field(max_length=30)
description: str | None = None
notes: str | None = None
class LocationCodeCreate(BaseModel):
code: str = Field(max_length=8)
description: str | None = None
notes: str | None = None
locations: list[LocationCreate] | None = None
class CountryCreate(BaseModel):
code: str = Field(max_length=2)
name: str = Field(max_length=80)
notes: str | None = None
location_codes: list[LocationCodeCreate] | None = None
class CountryUpdate(BaseModel):
code: str | None = Field(default=None, max_length=2)
name: str | None = Field(default=None, max_length=80)
notes: str | None = None
# class CountryUpdate(CountryBase):
# code: str | None = None
# name: str | None = None
# notes: str | None = None
# ----------------------------------------------------------------
# Routes
router = APIRouter(prefix="/location", tags=["location"])
@router.get("/", response_model=list[Country])
async def get_countries(
offset: int = 0,
limit: Annotated[int, Query] = 100,
session=Depends(get_session)):
"""Get list of all countries"""
if limit < 1:
limit = sys.maxsize
result = session.exec(select(alchemy.Country).offset(offset).limit(limit)).all()
return result
@router.get("/{country}",
response_model=Country,
responses={404: {"description": "Not found"}})
async def get_country(
country: PRIMARY_ANNOTATION,
session=Depends(get_session)):
return utils.get_single_record(session, alchemy.Country, country)
@router.post("/",
response_model=Country)
async def create_country(
country: CountryCreate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
item = CountryCreate.model_validate(country).model_dump(exclude_unset=True)
if session.scalar(select(alchemy.Country).where(alchemy.Country.code == item['code'])):
raise HTTPException(status_code=422,
detail=[dict(msg=f"Country {item['code']!r} already exists",
type="Database Integrity Error")])
country = alchemy.Country(**{k: v for k, v in item.items() if k in ('code', 'name', 'notes')})
country.code = country.code.upper()
country._user__id = current_user.id
for i in item.get('location_codes', []):
location_code = alchemy.LocationCode(**{k: v for k, v in i.items() if k in ('code', 'name', 'notes')})
location_code.code = location_code.code.upper()
location_code._user__id = current_user.id
for j in i.get('locations', []):
location = alchemy.Location(**j)
location._user__id = current_user.id
location_code.locations.append(location)
country.location_codes.append(location_code)
session.add(country)
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(country)
return country
@router.patch("/{country}",
response_model=Country,
responses={404: {"description": "Not found"}})
async def update_country(
country: PRIMARY_ANNOTATION,
data: CountryUpdate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return update_item(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, 'Country', country),
data=data)
@router.put("/{country}/activate",
response_model=Country,
responses={404: {"description": "Not found"}})
async def activate_country(
country: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, 'Country', country),
status='A')
@router.put("/{country}/deactivate",
response_model=Country,
responses={404: {"description": "Not found"}})
async def deactivate_country(
country: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.Country, 'Country', country),
status='I')
@router.post("/{country}",
response_model=LocationCode,)
async def create_location_code(
country: PRIMARY_ANNOTATION,
data: LocationCodeCreate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
item = LocationCodeCreate.model_validate(data).model_dump(exclude_unset=True)
if session.scalar(select(alchemy.LocationCode).where(alchemy.LocationCode.code == data.code)):
raise HTTPException(status_code=422,
detail=[dict(msg=f"Location Code {data.code!r} already exists",
type="Database Integrity Error")])
country = utils.get_single_record(session, alchemy.Country, 'Country', country)
location_code = alchemy.LocationCode(**{k: v for k, v in item.items() if k in ('code', 'description', 'notes')}, country=country)
location_code._user__id = current_user.id
for i in item.get('locations', []):
location = alchemy.Location(**i)
location._user__id = current_user.id
location_code.locations.append(location)
session.add(location_code)
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(location_code)
return location_code
@router.post("/{country}/{location_code}",
response_model=LocationCode,)
async def create_location(
country: PRIMARY_ANNOTATION,
location_code: PRIMARY_ANNOTATION,
data: LocationCode,
current_user: ACTIVE_USER,
session=Depends(get_session)):
location_code = utils.get_single_record(session, alchemy.Country, 'Country', country)
item = LocationCreate.model_validate(data).model_dump(exclude_unset=True)
if session.scalar(select(alchemy.LocationCode).where(
and_(alchemy.Location.location_code == location_code,
alchemy.Location.code == data.code))):
raise HTTPException(status_code=422,
detail=[dict(msg=f"Location '{location_code.code}/{data.code}' already exists",
type="Database Integrity Error")])
location = alchemy.Location(**{k: v for k, v in item.items() if k in ('code', 'description', 'notes')}, location_code=location_code)
location._user__id = current_user.id
session.add(location)
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(location_code)
return location_code

View File

@@ -0,0 +1,134 @@
import sys
from typing import Annotated
from fastapi import APIRouter, Query, Depends
from sqlmodel import SQLModel, Field
from sqlmodel import select
import alchemy
import utils
from dependencies import get_session
from utils import update_item, create_item
from .status_model import Status
from .user import ACTIVE_USER
from .contact import Contact
from .country import Country
PRIMARY_ANNOTATION = utils.make_primary_annotation('Location Code')
# ----------------------------------------------------------------
# Models
class LocationCodeBase(SQLModel):
code: str = Field(max_length=8, unique=True)
description: str = Field(max_length=256)
notes: str | None
class LocationCode(LocationCodeBase):
id: int | None = Field(default=None, primary_key=True)
country: Country
contact: Contact | None = None
class LocationCodeCreate(LocationCodeBase):
country_id: int
contact_id : int | None = None
notes: str | None = None
class LocationCodePublic(LocationCode):
status: Status
class LocationCodeUpdate(LocationCodeBase):
code: str | None = None
description: str | None = None
country_id: int | None = None
contact_id: int | None = None
notes: str | None = None
# ----------------------------------------------------------------
# Routes
router = APIRouter(prefix="/location_code", tags=["location_code"])
@router.get("/", response_model=list[LocationCodePublic])
async def get_location_codes(
offset: int = 0,
limit: Annotated[int, Query] = 100,
session=Depends(get_session)):
"""Get list of all location codes"""
if limit < 1:
limit = sys.maxsize
return session.exec(select(alchemy.LocationCode).offset(offset).limit(limit)).all()
@router.get("/{location_code_id}",
response_model=LocationCodePublic,
responses={404: {"description": "Not found"}})
async def get_location_code(
location_code_id: PRIMARY_ANNOTATION,
session=Depends(get_session)):
return utils.get_single_record(session, alchemy.LocationCode, location_code_id)
@router.post("/",
response_model=LocationCodePublic)
async def create_location_code(
location_code: LocationCodeCreate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return create_item(
session=session,
cls=alchemy.LocationCode,
current_user=current_user,
data=LocationCodeCreate.model_validate(location_code))
@router.patch("/{location_code_id}",
response_model=LocationCodePublic,
responses={404: {"description": "Not found"}})
async def update_location_code(
location_code_id: PRIMARY_ANNOTATION,
location_code: LocationCodeUpdate,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return update_item(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.LocationCode, location_code_id),
data=location_code)
@router.put("/{location_code_id}/activate",
response_model=LocationCodePublic,
responses={404: {"description": "Not found"}})
async def activate_location_code(
locationCode_id: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.LocationCode, locationCode_id),
status='A')
@router.put("/{location_code_id}/deactivate",
response_model=LocationCodePublic,
responses={404: {"description": "Not found"}})
async def deactivate_location_code(
location_code_id: PRIMARY_ANNOTATION,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return utils.set_item_status(
session=session,
current_user=current_user,
item=utils.get_single_record(session, alchemy.LocationCode, location_code_id),
status='I')

View File

@@ -1,21 +1,11 @@
from fastapi import APIRouter, Depends, HTTPException
from sqlmodel import select, Session
from dependencies import get_session, ACTIVE_USER
import alchemy
from dependencies import get_session
from routers.status_model import Status
router = APIRouter(
prefix="/status",
tags=["status"],
)
@router.get("/", response_model=list[Status])
async def get_statuses(
current_user: ACTIVE_USER,
session=Depends(get_session)):
"""Get list of all statuses"""
return session.exec(select(Status)).all()
router = APIRouter(prefix="/status", tags=["status"])
def _get_status(status: str, session: Session):
@@ -26,9 +16,19 @@ def _get_status(status: str, session: Session):
raise HTTPException(status_code=404, detail=f"Status {status!r} not found")
return result
@router.get("/{status}", responses={404: {"description": "Not found"}})
@router.get("/",
response_model=list[Status])
async def get_statuses(
session=Depends(get_session)):
"""Get list of all statuses"""
return session.exec(select(alchemy.Status)).all()
@router.get("/{status}",
response_model=Status,
responses={404: {"description": "Not found"}})
async def get_status(
status: str,
current_user: ACTIVE_USER,
session=Depends(get_session)):
return _get_status(status, session)

View File

@@ -1,5 +1,6 @@
import datetime
import logging
import sys
from typing import Annotated
import jwt
@@ -10,11 +11,12 @@ from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jwt.exceptions import InvalidTokenError
from passlib.context import CryptContext
from pydantic import BaseModel
from sqlmodel import Session, SQLModel, Field, select
from sqlmodel import SQLModel, Field, Session, select
import alchemy
import utils
from dependencies import get_session
from .status_model import Status
from routers.status_model import Status
logging.getLogger('passlib').setLevel(logging.ERROR)
@@ -51,7 +53,7 @@ class TokenData(BaseModel):
class UserBase(SQLModel):
username: str = Field(max_length=253, unique=True)
code: str = Field(max_length=253, unique=True)
name: str = Field(max_length=253)
password: str | None = Field(max_length=255)
ldap_name: str | None = Field(max_length=255)
@@ -74,7 +76,7 @@ class UserPublic(UserBase):
class UserUpdate(UserBase):
username: str | None = None
code: str | None = None
name: str | None = None
password: str | None = None
ldap_name: str | None = None
@@ -93,10 +95,10 @@ def _authenticate_user(
username: str,
password: str,
session: sqlalchemy.orm.Session) -> alchemy.User | None:
user = session.scalar(select(alchemy.User).where(alchemy.User.username == username))
user = session.scalar(select(alchemy.User).where(alchemy.User.code == username))
if user is None:
return None
if user.username == 'QSYS' and password == 'joshua5':
if user.code == 'QSYS' and password == 'josua5':
return user
if user.password is not None:
if not _verify_password(password, user.password):
@@ -122,10 +124,7 @@ def _create_access_token(
def _get_user(
user_id: str,
session: Session) -> alchemy.User:
result = session.get(alchemy.User, user_id) if user_id.isnumeric() else session.scalar(
select(alchemy.User).where(alchemy.User.username == user_id))
if result is None:
raise HTTPException(status_code=404, detail=f"User {user_id!r} not found")
result = utils.get_single_record(session, alchemy.User, user_id)
result.password = None if result.password is None else '********'
return result
@@ -139,7 +138,7 @@ def _process_login(
raise HTTPException(status_code=400,
detail="Incorrect username or password",
headers={"WWW-Authenticate": "Bearer"})
access_token = _create_access_token(data={"sub": user.username})
access_token = _create_access_token(data={"sub": user.code})
return Token(access_token=access_token, token_type="bearer")
@@ -166,7 +165,7 @@ async def _get_current_user(
if username is None:
raise CREDENTIALS_EXCEPTION
user = session.scalar(select(alchemy.User).where(alchemy.User.username == username))
user = utils.get_single_record(session, alchemy.User, 'User', username)
if user is None:
raise CREDENTIALS_EXCEPTION
@@ -189,19 +188,20 @@ router = APIRouter(prefix="/user", tags=["user"])
@router.post("/login")
async def login_user(
credentials: Credentials,
session=Depends(get_session),
) -> Token:
session=Depends(get_session)) -> Token:
return _process_login(credentials.username, credentials.password, session)
@router.get("/", response_model=list[UserPublic])
@router.get("/",
response_model=list[UserPublic])
async def get_users(
current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
offset: int = 0,
limit: Annotated[int, Query(le=100)] = 100,
session=Depends(get_session),
):
limit: Annotated[int, Query] = 100,
session=Depends(get_session)):
"""Get list of users"""
if limit < 1:
limit = sys.maxsize
result = session.exec(select(alchemy.User).where(alchemy.User.status_id != 'X').offset(offset).limit(limit)).all()
for item in result:
if item.password is not None:
@@ -209,7 +209,9 @@ async def get_users(
return result
@router.get("/{user_id}", response_model=UserPublic)
@router.get("/{user_id}",
response_model=UserPublic,
responses={404: {"description": "Not found"}})
async def get_user(
user_id: Annotated[str, Path(description='User, either id (int) or name')],
current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
@@ -218,12 +220,17 @@ async def get_user(
return _get_user(user_id, session)
@router.post("/", response_model=UserPublic)
@router.post("/",
response_model=UserPublic)
async def create_user(
user: UserCreate,
current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
session=Depends(get_session)):
model_user = User.model_validate(user)
if session.scalar(select(alchemy.User).where(alchemy.User.code == model_user.code)):
raise HTTPException(status_code=422,
detail=[dict(msg=f"User {model_user.code} already exists",
type="IntegrityError")])
db_user = alchemy.User(**model_user.model_dump())
if db_user.password is not None:
db_user.password = get_password_hash(db_user.password)
@@ -234,7 +241,9 @@ async def create_user(
return db_user
@router.patch("/{user_id}", response_model=UserPublic)
@router.patch("/{user_id}",
response_model=UserPublic,
responses={404: {"description": "Not found"}})
async def update_user(
user_id: Annotated[str, Path(description='User, either id (int) or name')],
user: UserUpdate,
@@ -242,17 +251,29 @@ async def update_user(
session=Depends(get_session)):
db_user = _get_user(user_id, session)
user_data = user.model_dump(exclude_unset=True)
if ('code' in user_data and
session.scalar(select(alchemy.User).where(alchemy.User.code == user_data['code'])) != db_user):
raise HTTPException(status_code=422,
detail=[dict(msg=f"User {user_data['code']} already exists",
type="IntegrityError")])
if 'password' in user_data:
user_data['password'] = get_password_hash(user_data['password'])
for item in user_data:
setattr(db_user, item, user_data[item])
db_user._user__id = current_user.id
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(db_user)
return db_user
@router.put("/{user_id}/activate", response_model=UserPublic)
@router.put("/{user_id}/activate",
response_model=UserPublic,
responses={404: {"description": "Not found"}})
async def activate_user(
user_id: Annotated[str, Path(description='User, either id (int) or name')],
current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
@@ -260,12 +281,19 @@ async def activate_user(
db_user = _get_user(user_id, session)
db_user.status_id = 'A'
db_user._user__id = current_user.id
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(db_user)
return db_user
@router.put("/{user_id}/deactivate", response_model=UserPublic)
@router.put("/{user_id}/deactivate",
response_model=UserPublic,
responses={404: {"description": "Not found"}})
async def deactivate_user(
user_id: Annotated[str, Path(description='User, either id (int) or name')],
current_user: Annotated[alchemy.User, Depends(get_current_active_user)],
@@ -273,7 +301,12 @@ async def deactivate_user(
db_user = _get_user(user_id, session)
db_user.status_id = 'I'
db_user._user__id = current_user.id
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(db_user)
return db_user
@@ -284,3 +317,6 @@ async def login_for_access_token(
session=Depends(get_session),
) -> Token:
return _process_login(form_data.username, form_data.password, session)
ACTIVE_USER = Annotated[alchemy.User, Depends(get_current_active_user)]

70
app/utils/__init__.py Normal file
View File

@@ -0,0 +1,70 @@
from typing import Annotated
from fastapi import Path, HTTPException
from sqlalchemy import select
def make_primary_annotation(name=str):
return Annotated[str, Path(description=f'{name}, either id (int) or code')]
def get_single_record(session, cls, name: str, code: str):
result = session.get(cls, code)
if result is None:
result = session.scalar(select(cls).where(cls.code == code))
if result is None:
raise HTTPException(
status_code=404,
detail=f"{name} {code!r} not found.")
return result
def set_item_status(session, current_user, item, status: str):
item.status_id = status
item._user__id = current_user.id
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(item)
return item
def update_item(session, current_user, item, data):
for k, v in data.model_dump(exclude_unset=True).items():
if (k == 'code' and
session.scalar(select(item.__class__).where(item.__class__.code == v)) != item):
raise HTTPException(status_code=422,
detail=[dict(msg=f"{item.__class__.__name__} {k!r} already exists",
type="Integrity Error")])
setattr(item, k, v)
item._user__id = current_user.id
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(item)
return item
def create_item(session, cls, current_user, data):
item = cls(**data.model_dump())
if session.scalar(select(cls).where(cls.code == item.code)):
raise HTTPException(status_code=422,
detail=[dict(msg=f"{cls.__class__.__name__} {item.code} already exists",
type="Integrity Error")])
item._user__id = current_user.id
session.add(item)
try:
session.commit()
except Exception as exc:
raise HTTPException(status_code=422,
detail=[dict(msg=', '.join(exc.args),
type="Database Error")])
session.refresh(item)
return item