Skip to content

Commit 6120623

Browse files
authored
Merge pull request #8 from shiningflash/loki-grafana
Improvements and refinements
2 parents 4cac65c + 6b6ba78 commit 6120623

5 files changed

Lines changed: 188 additions & 75 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -175,3 +175,5 @@ cython_debug/
175175
.pypirc
176176

177177
.venv
178+
179+
.ruff_cache/

app/api/mail.py

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,25 +4,25 @@
44
from app.services.mail import send_email_background
55

66

7-
mail_router = APIRouter(prefix='/send-email', tags=['Send Mail'])
7+
mail_router = APIRouter(prefix="/send-email", tags=["Send Mail"])
88

99

10-
@mail_router.get('/send-email/asynchronous')
10+
@mail_router.get("/send-email/asynchronous")
1111
async def send_email_asynchronous():
12-
await send_email_async(
13-
subject='Invitation to Join Our Company X',
14-
email_to='amirul@gmail.com',
15-
body={"title": "Invitation", "name": "Dear Mr. X", "invitation_url": ''}
12+
await send_email_async(
13+
subject="Invitation to Join Our Company X",
14+
email_to="amirul@gmail.com",
15+
body={"title": "Invitation", "name": "Dear Mr. X", "invitation_url": ""},
1616
)
17-
return 'Success'
17+
return "Success"
1818

1919

20-
@mail_router.get('/backgroundtasks')
20+
@mail_router.get("/backgroundtasks")
2121
def send_email_backgroundtasks(background_tasks: BackgroundTasks):
22-
send_email_background(
23-
background_tasks=background_tasks,
24-
subject='Invitation to Join Our Company X',
25-
email_to='amirul@gmail.com',
26-
body={"title": "Invitation", "name": "Dear Mr. X"}
22+
send_email_background(
23+
background_tasks=background_tasks,
24+
subject="Invitation to Join Our Company X",
25+
email_to="amirul@gmail.com",
26+
body={"title": "Invitation", "name": "Dear Mr. X"},
2727
)
28-
return 'Success'
28+
return "Success"

app/api/token.py

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
11
# import logging
22
from datetime import timedelta
3+
34
from fastapi import APIRouter, Depends, HTTPException, status
4-
from fastapi.responses import JSONResponse
55
from fastapi.encoders import jsonable_encoder
6+
from fastapi.responses import JSONResponse
67

7-
from app.db.base import get_db
88
from app import schemas
9-
from app.services.oauth_client import OAuth2ClientCredentialsRequestForm, create_client_access_token, get_subject_from_token
109
from app.services.auth import validate_client_credentials
10+
from app.services.oauth_client import (
11+
OAuth2ClientCredentialsRequestForm,
12+
create_client_access_token,
13+
)
1114

12-
13-
token_router = APIRouter(tags=['Token'])
14-
# logger = logging.getLogger(__name__)
15+
token_router = APIRouter(tags=["Token"])
1516

1617

1718
@token_router.post("/token", response_model=schemas.ClientTokenResponse)
@@ -21,9 +22,17 @@ def get_token(form_data: OAuth2ClientCredentialsRequestForm = Depends()):
2122
scopes = form_data.scopes
2223

2324
if validate_client_credentials(client_id, client_secret, scopes[0]):
24-
access_token = create_client_access_token(data={"sub": client_id, "scope": scopes[0]}, secret_key=client_secret, expires_delta=timedelta(hours=2))
25-
response = schemas.ClientTokenResponse(access_token=access_token, token_type="bearer")
25+
access_token = create_client_access_token(
26+
data={"sub": client_id, "scope": scopes[0]},
27+
secret_key=client_secret,
28+
expires_delta=timedelta(hours=2),
29+
)
30+
response = schemas.ClientTokenResponse(
31+
access_token=access_token, token_type="bearer"
32+
)
2633

27-
return JSONResponse(status_code=status.HTTP_200_OK, content=jsonable_encoder(response))
34+
return JSONResponse(
35+
status_code=status.HTTP_200_OK, content=jsonable_encoder(response)
36+
)
2837
else:
2938
raise HTTPException(status_code=403, detail="Invalid client credentials")

app/api/user.py

Lines changed: 20 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -1,46 +1,40 @@
1+
from datetime import datetime
2+
from typing import List
3+
14
from fastapi import APIRouter, HTTPException, status
25
from fastapi.params import Depends
36
from sqlalchemy.orm import Session
4-
from datetime import datetime
5-
from typing import List
67

78
from app import models, schemas
89
from app.db.base import get_db
910
from app.db.crud import CRUDBase
11+
from app.services.oauth2 import add_new_role_in_org
1012
from app.utils.invitation import confirm_invitation_token
1113
from app.utils.security import get_password_hash
12-
from app.services.oauth2 import add_new_role_in_org
1314

14-
user_router = APIRouter(prefix='/user', tags=['User'])
15+
user_router = APIRouter(prefix="/user", tags=["User"])
1516
user_crud = CRUDBase(model=models.User)
1617

1718

18-
@user_router.post('', response_model=schemas.UserDetails)
19-
def create_user(
20-
data: schemas.UserCreateRequest,
21-
db: Session = Depends(get_db)
22-
):
19+
@user_router.post("", response_model=schemas.UserDetails)
20+
def create_user(data: schemas.UserCreateRequest, db: Session = Depends(get_db)):
2321
token_data = confirm_invitation_token(token=data.token)
24-
invitation = db.query(models.Invitation
25-
).filter_by(unique_token=data.token
26-
).first()
22+
invitation = db.query(models.Invitation).filter_by(unique_token=data.token).first()
2723

2824
if data.password != data.confirm_password:
29-
raise HTTPException(status_code=400, detail='Password did not match')
25+
raise HTTPException(status_code=400, detail="Password did not match")
3026

31-
if (not token_data or not invitation or
32-
invitation.expires_at < datetime.now()):
27+
if not token_data or not invitation or invitation.expires_at < datetime.now():
3328
raise HTTPException(
34-
status_code=400,
35-
detail='Invalid information or expired invitation link')
29+
status_code=400, detail="Invalid information or expired invitation link"
30+
)
3631

37-
existing_user = db.query(models.User
38-
).filter(models.User.email == invitation.email
39-
).first()
32+
existing_user = (
33+
db.query(models.User).filter(models.User.email == invitation.email).first()
34+
)
4035
if existing_user:
4136
raise HTTPException(
42-
status_code=status.HTTP_400_BAD_REQUEST,
43-
detail="Email already exists"
37+
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already exists"
4438
)
4539

4640
user = schemas.UserCreate(
@@ -50,22 +44,18 @@ def create_user(
5044
organizational_role=invitation.organizational_role,
5145
password=get_password_hash(data.password),
5246
role=invitation.role,
53-
invited_by_id=invitation.created_by_id
47+
invited_by_id=invitation.created_by_id,
5448
)
5549
user.model_dump()
5650
user_dict = user_crud.create(db=db, obj_in=user)
5751

5852
# Add role to the new user
59-
add_new_role_in_org(
60-
user.email, invitation.role, invitation.organization, db
61-
)
53+
add_new_role_in_org(user.email, invitation.role, invitation.organization, db)
6254

6355
return user_dict
6456

6557

66-
@user_router.get('')
67-
def get_users(
68-
db: Session = Depends(get_db)
69-
) -> List[schemas.UserList]:
58+
@user_router.get("")
59+
def get_users(db: Session = Depends(get_db)) -> List[schemas.UserList]:
7060
db_users = db.query(models.User)
7161
return db_users

app/db/crud.py

Lines changed: 134 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1+
from datetime import datetime
12
from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union
23

34
from fastapi.encoders import jsonable_encoder
45
from pydantic import BaseModel
56
from sqlalchemy.orm import Session
6-
from datetime import datetime
77

88
from app.db.base_class import Base
99

@@ -13,62 +13,174 @@
1313

1414

1515
class CRUDBase(Generic[ModelType, CreateSchemaType, UpdateSchemaType]):
16-
def __init__(self, *, model: Type[ModelType]):
16+
"""
17+
Generic CRUD operations for SQLAlchemy models.
18+
19+
Provides standard Create, Read, Update, Delete operations with type safety.
20+
"""
21+
22+
def __init__(self, *, model: Type[ModelType]) -> None:
1723
"""
18-
CRUD object with default methods to Create, Read, Update, Delete (CRUD).
19-
**Parameters**
20-
* `model`: A SQLAlchemy model class
21-
* `schema`: A Pydantic model (schema) class
24+
Initialize CRUD operations for a specific model.
25+
26+
Args:
27+
model: A SQLAlchemy model class
2228
"""
2329
self.model = model
2430

2531
def get(self, db: Session, id: str) -> Optional[ModelType]:
32+
"""
33+
Retrieve a single record by primary key.
34+
35+
Args:
36+
db: Database session
37+
id: Primary key value
38+
39+
Returns:
40+
Model instance or None if not found
41+
"""
2642
return db.query(self.model).filter(self.model.id == id).first()
2743

2844
def get_by_field(self, db: Session, field: str, value: Any) -> Optional[ModelType]:
45+
"""
46+
Retrieve a single record by field value.
47+
48+
Args:
49+
db: Database session
50+
field: Field name to filter by
51+
value: Value to match
52+
53+
Returns:
54+
Model instance or None if not found
55+
56+
Raises:
57+
AttributeError: If field doesn't exist on model
58+
"""
59+
if not hasattr(self.model, field):
60+
raise AttributeError(f"Model {self.model.__name__} has no field '{field}'")
61+
2962
return db.query(self.model).filter(getattr(self.model, field) == value).first()
3063

3164
def get_multi_by_field(
32-
self, db: Session, field: str, value: Any
33-
) -> Optional[ModelType]:
34-
return db.query(self.model).filter(getattr(self.model, field) == value).all()
65+
self, db: Session, field: str, value: Any, *, skip: int = 0, limit: int = 100
66+
) -> List[ModelType]:
67+
"""
68+
Retrieve multiple records by field value.
69+
70+
Args:
71+
db: Database session
72+
field: Field name to filter by
73+
value: Value to match
74+
skip: Number of records to skip
75+
limit: Maximum number of records to return
76+
77+
Returns:
78+
List of model instances
79+
80+
Raises:
81+
AttributeError: If field doesn't exist on model
82+
"""
83+
if not hasattr(self.model, field):
84+
raise AttributeError(f"Model {self.model.__name__} has no field '{field}'")
85+
86+
return (
87+
db.query(self.model)
88+
.filter(getattr(self.model, field) == value)
89+
.offset(skip)
90+
.limit(limit)
91+
.all()
92+
)
3593

3694
def get_multi(
3795
self, db: Session, *, skip: int = 0, limit: int = 100
3896
) -> List[ModelType]:
97+
"""
98+
Retrieve multiple records with pagination.
99+
100+
Args:
101+
db: Database session
102+
skip: Number of records to skip
103+
limit: Maximum number of records to return
104+
105+
Returns:
106+
List of model instances
107+
"""
39108
return db.query(self.model).offset(skip).limit(limit).all()
40109

41110
def create(self, db: Session, *, obj_in: CreateSchemaType) -> ModelType:
111+
"""
112+
Create a new record in the database.
113+
114+
Args:
115+
db: Database session
116+
obj_in: Pydantic schema with creation data
117+
118+
Returns:
119+
Created model instance
120+
"""
42121
obj_in_data = jsonable_encoder(obj_in)
43-
db_obj = self.model(**obj_in_data) # type: ignore
122+
db_obj = self.model(**obj_in_data)
44123
db.add(db_obj)
45124
db.commit()
46125
db.refresh(db_obj)
47126
return db_obj
48127

49128
def update(
50-
self, db: Session, *, obj_in: Union[UpdateSchemaType, Dict[str, Any]]
129+
self,
130+
db: Session,
131+
*,
132+
db_obj: ModelType,
133+
obj_in: Union[UpdateSchemaType, Dict[str, Any]],
51134
) -> ModelType:
52-
db_obj = db.query(self.model).get(obj_in.id)
53-
obj_data = jsonable_encoder(db_obj)
135+
"""
136+
Update an existing record in the database.
137+
138+
Args:
139+
db: Database session
140+
db_obj: Database object to update
141+
obj_in: Pydantic schema or dict with updated data
142+
143+
Returns:
144+
Updated model instance
145+
"""
146+
# Convert input to dictionary
54147
if isinstance(obj_in, dict):
55148
update_data = obj_in
56149
else:
57-
update_data = jsonable_encoder(obj_in)
58-
59-
# Manually set updated_at to current time
60-
update_data['updated_at'] = datetime.utcnow()
61-
62-
for field in obj_data:
63-
if field in update_data:
64-
setattr(db_obj, field, update_data[field])
150+
update_data = jsonable_encoder(obj_in, exclude_unset=True)
151+
152+
# Update fields that exist in input
153+
for field, value in update_data.items():
154+
if hasattr(db_obj, field):
155+
setattr(db_obj, field, value)
156+
157+
# Update timestamp if model has updated_at field
158+
if hasattr(db_obj, "updated_at"):
159+
db_obj.updated_at = datetime.utcnow()
160+
65161
db.add(db_obj)
66162
db.commit()
67163
db.refresh(db_obj)
68164
return db_obj
69165

70166
def remove(self, db: Session, *, id: int) -> ModelType:
71-
obj = db.query(self.model).get(id)
167+
"""
168+
Delete a record from the database.
169+
170+
Args:
171+
db: Database session
172+
id: Primary key of record to delete
173+
174+
Returns:
175+
Deleted model instance
176+
177+
Raises:
178+
ValueError: If record not found
179+
"""
180+
obj = db.query(self.model).filter(self.model.id == id).first()
181+
if not obj:
182+
raise ValueError(f"Record with id {id} not found")
183+
72184
db.delete(obj)
73185
db.commit()
74186
return obj

0 commit comments

Comments
 (0)