Skip to content

Commit

Permalink
add organizations and projects routes
Browse files Browse the repository at this point in the history
  • Loading branch information
habibasseiss committed Nov 23, 2024
1 parent a4ad81f commit 548c036
Show file tree
Hide file tree
Showing 10 changed files with 200 additions and 13 deletions.
2 changes: 1 addition & 1 deletion .python-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
3.11
3.13
2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"request": "launch",
"module": "uvicorn",
"args": [
"app.main:app",
"app.api:api",
"--reload"
],
"cwd": "${workspaceFolder}",
Expand Down
1 change: 1 addition & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"files.exclude": {
"**/__pycache__": true,
"**/.*_cache": true,
},
"python.testing.pytestArgs": [
"tests"
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM python:3.11-slim
FROM python:3.13-slim

ENV PYTHONUNBUFFERED=1

Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
# Generating secrets
## Autogenerate migration

```python
```sh
uv run alembic revision --autogenerate -m "add ... table"
```

## Generating secrets

```sh
python -c "import secrets; print(secrets.token_urlsafe(32))"
```
3 changes: 2 additions & 1 deletion app/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@

from fastapi import FastAPI

from app.routers import auth, todos, users
from app.routers import auth, organizations, todos, users
from app.schemas import Message

api = FastAPI()

api.include_router(users.router)
api.include_router(auth.router)
api.include_router(todos.router)
api.include_router(organizations.router)


@api.get('/', status_code=HTTPStatus.OK, response_model=Message)
Expand Down
3 changes: 2 additions & 1 deletion app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,5 +117,6 @@ class Project:

# Many-to-one relationship
organization: Mapped[Organization] = relationship(
'Organization', back_populates='projects'
'Organization',
back_populates='projects',
)
151 changes: 151 additions & 0 deletions app/routers/organizations.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
from http import HTTPStatus
from typing import Annotated
from uuid import UUID

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session

from app.database import get_session
from app.models import Organization, Project, User
from app.schemas import (
OrganizationList,
ProjectList,
ProjectPublic,
ProjectSchema,
)
from app.security import get_current_user

router = APIRouter(prefix='/organizations', tags=['organizations'])

DbSession = Annotated[Session, Depends(get_session)]
CurrentUser = Annotated[User, Depends(get_current_user)]


# Utility functions
def get_organization(
session: DbSession, user: CurrentUser, organization_id: UUID
) -> Organization:
query = select(Organization).where(Organization.users.contains(user))
if organization_id:
query = query.where(Organization.id == organization_id)

organization = session.scalar(query)
if not organization:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail='Organization not found.',
)
return organization


def get_project(
session: DbSession,
user: CurrentUser,
organization_id: UUID,
project_id: UUID,
) -> Project:
project = session.scalar(
select(Project).where(
Project.organization_id == organization_id,
Project.id == project_id,
Project.organization.has(Organization.users.contains(user)),
)
)
if not project:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND,
detail='Project not found.',
)
return project


# Routes
@router.get('/', response_model=OrganizationList)
def list_organizations(session: DbSession, user: CurrentUser):
organizations = session.scalars(
select(Organization).where(Organization.users.contains(user))
).all()
return {'organizations': organizations}


@router.get('/{organization_id}/projects', response_model=ProjectList)
def list_organization_projects(
organization_id: UUID, session: DbSession, user: CurrentUser
):
organization = get_organization(session, user, organization_id)
projects = session.scalars(
select(Project).where(Project.organization_id == organization.id)
).all()
return {'projects': projects}


@router.post(
'/{organization_id}/projects',
response_model=ProjectPublic,
status_code=HTTPStatus.CREATED,
)
def create_project(
organization_id: UUID,
project: ProjectSchema,
session: DbSession,
user: CurrentUser,
):
organization = get_organization(session, user, organization_id)

db_project = Project( # type: ignore
name=project.name,
description=project.description,
organization_id=organization_id,
organization=organization,
)
session.add(db_project)
session.commit()
session.refresh(db_project)

return db_project


@router.get(
'/{organization_id}/projects/{project_id}', response_model=ProjectPublic
)
def read_project(
organization_id: UUID,
project_id: UUID,
session: DbSession,
user: CurrentUser,
):
return get_project(session, user, organization_id, project_id)


@router.put(
'/{organization_id}/projects/{project_id}', response_model=ProjectPublic
)
def update_project(
organization_id: UUID,
project_id: UUID,
project: ProjectSchema,
session: DbSession,
user: CurrentUser,
):
db_project = get_project(session, user, organization_id, project_id)
db_project.name = project.name
db_project.description = project.description
session.commit()
session.refresh(db_project)
return db_project


@router.delete(
'/{organization_id}/projects/{project_id}',
status_code=HTTPStatus.NO_CONTENT,
)
def delete_project(
organization_id: UUID,
project_id: UUID,
session: DbSession,
user: CurrentUser,
):
project = get_project(session, user, organization_id, project_id)
session.delete(project)
session.commit()
13 changes: 11 additions & 2 deletions app/routers/users.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

from fastapi import APIRouter, Depends, HTTPException
from sqlalchemy import select
from sqlalchemy.orm import Session
from sqlalchemy.orm import Session, joinedload

from app.database import get_session
from app.models import Organization, User
Expand Down Expand Up @@ -53,7 +53,16 @@ def create_user(user: UserSchema, session: DbSession):

@router.get('/', response_model=UserList)
def read_users(session: DbSession, skip: int = 0, limit: int = 100):
users = session.scalars(select(User).offset(skip).limit(limit)).all()
users = (
session.scalars(
select(User)
.options(joinedload(User.organizations))
.offset(skip)
.limit(limit)
)
.unique()
.all()
)
return {'users': users}


Expand Down
26 changes: 22 additions & 4 deletions app/schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,20 @@ class Message(BaseModel):
message: str


class OrganizationSchema(BaseModel):
name: str


class OrganizationPublic(BaseModel):
id: UUID
name: str
model_config = ConfigDict(from_attributes=True)


class OrganizationList(BaseModel):
organizations: list[OrganizationPublic]


class UserSchema(BaseModel):
email: EmailStr
password: str
Expand All @@ -17,6 +31,7 @@ class UserSchema(BaseModel):
class UserPublic(BaseModel):
id: UUID
email: EmailStr
organizations: list[OrganizationPublic]
model_config = ConfigDict(from_attributes=True)


Expand Down Expand Up @@ -56,15 +71,18 @@ class TodoUpdate(BaseModel):
state: TodoState | None = None


class OrganizationSchema(BaseModel):
class ProjectSchema(BaseModel):
name: str
description: str


class OrganizationPublic(BaseModel):
class ProjectPublic(BaseModel):
id: UUID
name: str
description: str
organization_id: UUID
model_config = ConfigDict(from_attributes=True)


class OrganizationList(BaseModel):
organizations: list[OrganizationPublic]
class ProjectList(BaseModel):
projects: list[ProjectPublic]

0 comments on commit 548c036

Please sign in to comment.