Spaces:
Sleeping
Sleeping
shri-jai
commited on
Commit
·
fcdc4de
1
Parent(s):
119d5fc
feat: journaling endpoints
Browse files- alembic/versions/217db60578fa_added_journal_table.py +44 -0
- src/auth/utils.py +2 -2
- src/core/__init__.py +1 -0
- src/core/models.py +1 -0
- src/journaling/__init__.py +0 -0
- src/journaling/models.py +32 -0
- src/journaling/router.py +92 -0
- src/journaling/schemas.py +31 -0
- src/journaling/service.py +91 -0
- src/main.py +3 -0
alembic/versions/217db60578fa_added_journal_table.py
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""added journal table
|
| 2 |
+
|
| 3 |
+
Revision ID: 217db60578fa
|
| 4 |
+
Revises: d9b4df655a55
|
| 5 |
+
Create Date: 2025-12-05 14:19:31.722971
|
| 6 |
+
|
| 7 |
+
"""
|
| 8 |
+
from typing import Sequence, Union
|
| 9 |
+
|
| 10 |
+
from alembic import op
|
| 11 |
+
import sqlalchemy as sa
|
| 12 |
+
import sqlmodel.sql.sqltypes
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
# revision identifiers, used by Alembic.
|
| 16 |
+
revision: str = '217db60578fa'
|
| 17 |
+
down_revision: Union[str, Sequence[str], None] = 'd9b4df655a55'
|
| 18 |
+
branch_labels: Union[str, Sequence[str], None] = None
|
| 19 |
+
depends_on: Union[str, Sequence[str], None] = None
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def upgrade() -> None:
|
| 23 |
+
"""Upgrade schema."""
|
| 24 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 25 |
+
op.create_table('journal_entries',
|
| 26 |
+
sa.Column('id', sa.Uuid(), nullable=False),
|
| 27 |
+
sa.Column('user_id', sa.UUID(), nullable=False),
|
| 28 |
+
sa.Column('title', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
| 29 |
+
sa.Column('content', sqlmodel.sql.sqltypes.AutoString(), nullable=False),
|
| 30 |
+
sa.Column('journal_date', sa.Date(), nullable=False),
|
| 31 |
+
sa.Column('created_at', sa.DateTime(), nullable=False),
|
| 32 |
+
sa.Column('updated_at', sa.DateTime(), nullable=False),
|
| 33 |
+
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='CASCADE'),
|
| 34 |
+
sa.PrimaryKeyConstraint('id'),
|
| 35 |
+
sa.UniqueConstraint('user_id', 'journal_date', name='unique_user_date_journal')
|
| 36 |
+
)
|
| 37 |
+
# ### end Alembic commands ###
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
def downgrade() -> None:
|
| 41 |
+
"""Downgrade schema."""
|
| 42 |
+
# ### commands auto generated by Alembic - please adjust! ###
|
| 43 |
+
op.drop_table('journal_entries')
|
| 44 |
+
# ### end Alembic commands ###
|
src/auth/utils.py
CHANGED
|
@@ -156,7 +156,7 @@ async def verify_verification_token(token: str) -> str:
|
|
| 156 |
bearer_scheme = HTTPBearer()
|
| 157 |
|
| 158 |
|
| 159 |
-
def
|
| 160 |
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
|
| 161 |
):
|
| 162 |
"""Decode JWT token and extract current user ID"""
|
|
@@ -169,7 +169,7 @@ def get_current_user(
|
|
| 169 |
if user_id is None:
|
| 170 |
raise HTTPException(
|
| 171 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 172 |
-
detail="Invalid token:
|
| 173 |
)
|
| 174 |
return user_id
|
| 175 |
|
|
|
|
| 156 |
bearer_scheme = HTTPBearer()
|
| 157 |
|
| 158 |
|
| 159 |
+
def get_current_user(
|
| 160 |
credentials: HTTPAuthorizationCredentials = Depends(bearer_scheme),
|
| 161 |
):
|
| 162 |
"""Decode JWT token and extract current user ID"""
|
|
|
|
| 169 |
if user_id is None:
|
| 170 |
raise HTTPException(
|
| 171 |
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 172 |
+
detail="Invalid token: Missing user id",
|
| 173 |
)
|
| 174 |
return user_id
|
| 175 |
|
src/core/__init__.py
CHANGED
|
@@ -6,3 +6,4 @@ from src.home import models as home_models
|
|
| 6 |
from src.profile import models as profile_models
|
| 7 |
from src.wellbeing import models as wellbeing_models
|
| 8 |
from src.payslip import models as payslip_models
|
|
|
|
|
|
| 6 |
from src.profile import models as profile_models
|
| 7 |
from src.wellbeing import models as wellbeing_models
|
| 8 |
from src.payslip import models as payslip_models
|
| 9 |
+
from src.journaling import models as journaling_models
|
src/core/models.py
CHANGED
|
@@ -43,6 +43,7 @@ class Users(SQLModel, table=True):
|
|
| 43 |
created_at: datetime = Field(default_factory=datetime.now)
|
| 44 |
asset: List["Assets"] = Relationship(back_populates="user")
|
| 45 |
water_logs: List["WaterLogs"] = Relationship(back_populates="user")
|
|
|
|
| 46 |
|
| 47 |
|
| 48 |
class Teams(SQLModel, table=True):
|
|
|
|
| 43 |
created_at: datetime = Field(default_factory=datetime.now)
|
| 44 |
asset: List["Assets"] = Relationship(back_populates="user")
|
| 45 |
water_logs: List["WaterLogs"] = Relationship(back_populates="user")
|
| 46 |
+
journal_entries: List["JournalEntry"] = Relationship(back_populates="user")
|
| 47 |
|
| 48 |
|
| 49 |
class Teams(SQLModel, table=True):
|
src/journaling/__init__.py
ADDED
|
File without changes
|
src/journaling/models.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from datetime import date, datetime
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
from sqlalchemy.dialects.postgresql import UUID
|
| 6 |
+
from sqlalchemy import Column, ForeignKey, UniqueConstraint
|
| 7 |
+
from sqlmodel import SQLModel, Field, Relationship
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class JournalEntry(SQLModel, table=True):
|
| 11 |
+
__tablename__ = "journal_entries"
|
| 12 |
+
__table_args__ = (
|
| 13 |
+
UniqueConstraint("user_id", "journal_date", name="unique_user_date_journal"),
|
| 14 |
+
)
|
| 15 |
+
id: uuid.UUID = Field(default_factory=uuid.uuid4, primary_key=True)
|
| 16 |
+
user_id: uuid.UUID = Field(
|
| 17 |
+
sa_column=Column(
|
| 18 |
+
UUID(as_uuid=True),
|
| 19 |
+
ForeignKey("users.id", ondelete="CASCADE"),
|
| 20 |
+
nullable=False,
|
| 21 |
+
)
|
| 22 |
+
)
|
| 23 |
+
title: str = Field(nullable=False)
|
| 24 |
+
content: str = Field(nullable=False)
|
| 25 |
+
journal_date: date = Field(nullable=False)
|
| 26 |
+
created_at: datetime = Field(default_factory=datetime.now)
|
| 27 |
+
updated_at: datetime = Field(
|
| 28 |
+
default_factory=datetime.now,
|
| 29 |
+
sa_column_kwargs={"onupdate": datetime.now},
|
| 30 |
+
nullable=False,
|
| 31 |
+
)
|
| 32 |
+
user: Optional["Users"] = Relationship(back_populates="journal_entries")
|
src/journaling/router.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from fastapi import APIRouter, Depends, HTTPException
|
| 3 |
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 4 |
+
|
| 5 |
+
from src.core.database import get_async_session
|
| 6 |
+
from src.core.schemas import BaseResponse
|
| 7 |
+
from src.auth.utils import get_current_user
|
| 8 |
+
|
| 9 |
+
from src.journaling.schemas import (
|
| 10 |
+
JournalCreate,
|
| 11 |
+
JournalUpdate,
|
| 12 |
+
JournalResponse,
|
| 13 |
+
)
|
| 14 |
+
from src.journaling.service import (
|
| 15 |
+
create_or_update_journal,
|
| 16 |
+
get_all_journals,
|
| 17 |
+
get_journal,
|
| 18 |
+
update_journal,
|
| 19 |
+
delete_journal,
|
| 20 |
+
)
|
| 21 |
+
|
| 22 |
+
router = APIRouter(prefix="/journal", tags=["Journal"])
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
@router.post("/", response_model=BaseResponse[JournalResponse], status_code=200)
|
| 26 |
+
async def create_or_update(
|
| 27 |
+
data: JournalCreate,
|
| 28 |
+
user_id: uuid.UUID = Depends(get_current_user),
|
| 29 |
+
session: AsyncSession = Depends(get_async_session),
|
| 30 |
+
):
|
| 31 |
+
data.user_id = user_id
|
| 32 |
+
|
| 33 |
+
try:
|
| 34 |
+
record = await create_or_update_journal(data, session)
|
| 35 |
+
return BaseResponse(status_code=200, data=record)
|
| 36 |
+
except ValueError as e:
|
| 37 |
+
raise HTTPException(status_code=404, detail=str(e))
|
| 38 |
+
|
| 39 |
+
|
| 40 |
+
@router.get("/", response_model=BaseResponse[list[JournalResponse]], status_code=200)
|
| 41 |
+
async def list_user_journals(
|
| 42 |
+
user_id: uuid.UUID = Depends(get_current_user),
|
| 43 |
+
session: AsyncSession = Depends(get_async_session),
|
| 44 |
+
):
|
| 45 |
+
records = await get_all_journals(user_id, session)
|
| 46 |
+
return BaseResponse(status_code=200, data=records)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@router.get("/entry/{journal_id}", response_model=BaseResponse[JournalResponse], status_code=200)
|
| 50 |
+
async def fetch_single(
|
| 51 |
+
journal_id: uuid.UUID,
|
| 52 |
+
user_id: uuid.UUID = Depends(get_current_user),
|
| 53 |
+
session: AsyncSession = Depends(get_async_session),
|
| 54 |
+
):
|
| 55 |
+
try:
|
| 56 |
+
record = await get_journal(journal_id, user_id, session)
|
| 57 |
+
return BaseResponse(status_code=200, data=record)
|
| 58 |
+
except ValueError as e:
|
| 59 |
+
if str(e) == "Not authorized":
|
| 60 |
+
raise HTTPException(status_code=403, detail=str(e))
|
| 61 |
+
raise HTTPException(status_code=404, detail=str(e))
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
@router.put("/entry/{journal_id}", response_model=BaseResponse[JournalResponse], status_code=200)
|
| 65 |
+
async def update_entry(
|
| 66 |
+
journal_id: uuid.UUID,
|
| 67 |
+
data: JournalUpdate,
|
| 68 |
+
user_id: uuid.UUID = Depends(get_current_user),
|
| 69 |
+
session: AsyncSession = Depends(get_async_session),
|
| 70 |
+
):
|
| 71 |
+
try:
|
| 72 |
+
record = await update_journal(journal_id, data, user_id, session)
|
| 73 |
+
return BaseResponse(status_code=200, data=record)
|
| 74 |
+
except ValueError as e:
|
| 75 |
+
if str(e) == "Not authorized":
|
| 76 |
+
raise HTTPException(status_code=403, detail=str(e))
|
| 77 |
+
raise HTTPException(status_code=404, detail=str(e))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
@router.delete("/entry/{journal_id}", response_model=BaseResponse[str], status_code=200)
|
| 81 |
+
async def delete_entry(
|
| 82 |
+
journal_id: uuid.UUID,
|
| 83 |
+
user_id: uuid.UUID = Depends(get_current_user),
|
| 84 |
+
session: AsyncSession = Depends(get_async_session),
|
| 85 |
+
):
|
| 86 |
+
try:
|
| 87 |
+
await delete_journal(journal_id, user_id, session)
|
| 88 |
+
return BaseResponse(status_code=200, data="Deleted")
|
| 89 |
+
except ValueError as e:
|
| 90 |
+
if str(e) == "Not authorized":
|
| 91 |
+
raise HTTPException(status_code=403, detail=str(e))
|
| 92 |
+
raise HTTPException(status_code=404, detail=str(e))
|
src/journaling/schemas.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import uuid
|
| 2 |
+
from datetime import date, datetime
|
| 3 |
+
from typing import Optional
|
| 4 |
+
|
| 5 |
+
from sqlmodel import SQLModel
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class JournalBase(SQLModel):
|
| 9 |
+
title: str
|
| 10 |
+
content: str
|
| 11 |
+
journal_date: date
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class JournalCreate(JournalBase):
|
| 15 |
+
user_id: Optional[uuid.UUID] = None
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class JournalUpdate(SQLModel):
|
| 19 |
+
title: Optional[str] = None
|
| 20 |
+
content: Optional[str] = None
|
| 21 |
+
journal_date: Optional[date] = None
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
class JournalResponse(SQLModel):
|
| 25 |
+
id: uuid.UUID
|
| 26 |
+
user_id: uuid.UUID
|
| 27 |
+
title: str
|
| 28 |
+
content: str
|
| 29 |
+
journal_date: date
|
| 30 |
+
created_at: datetime
|
| 31 |
+
updated_at: datetime
|
src/journaling/service.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime
|
| 2 |
+
import uuid
|
| 3 |
+
from sqlmodel import select
|
| 4 |
+
from sqlmodel.ext.asyncio.session import AsyncSession
|
| 5 |
+
|
| 6 |
+
from src.core.models import Users
|
| 7 |
+
from src.journaling.models import JournalEntry
|
| 8 |
+
from src.journaling.schemas import JournalCreate, JournalUpdate
|
| 9 |
+
|
| 10 |
+
|
| 11 |
+
async def create_or_update_journal(data: JournalCreate, session: AsyncSession):
|
| 12 |
+
user = await session.exec(select(Users).where(Users.id == data.user_id))
|
| 13 |
+
if not user.first():
|
| 14 |
+
raise ValueError("User not found")
|
| 15 |
+
|
| 16 |
+
query = select(JournalEntry).where(
|
| 17 |
+
JournalEntry.user_id == data.user_id,
|
| 18 |
+
JournalEntry.journal_date == data.journal_date
|
| 19 |
+
)
|
| 20 |
+
existing = (await session.exec(query)).first()
|
| 21 |
+
|
| 22 |
+
if existing:
|
| 23 |
+
existing.title = data.title
|
| 24 |
+
existing.content = data.content
|
| 25 |
+
existing.updated_at = datetime.utcnow()
|
| 26 |
+
record = existing
|
| 27 |
+
else:
|
| 28 |
+
record = JournalEntry(
|
| 29 |
+
user_id=data.user_id,
|
| 30 |
+
title=data.title,
|
| 31 |
+
content=data.content,
|
| 32 |
+
journal_date=data.journal_date,
|
| 33 |
+
)
|
| 34 |
+
session.add(record)
|
| 35 |
+
|
| 36 |
+
await session.commit()
|
| 37 |
+
await session.refresh(record)
|
| 38 |
+
return record
|
| 39 |
+
|
| 40 |
+
|
| 41 |
+
async def get_all_journals(user_id: uuid.UUID, session: AsyncSession):
|
| 42 |
+
stmt = (
|
| 43 |
+
select(JournalEntry)
|
| 44 |
+
.where(JournalEntry.user_id == user_id)
|
| 45 |
+
.order_by(JournalEntry.journal_date.desc())
|
| 46 |
+
)
|
| 47 |
+
result = await session.exec(stmt)
|
| 48 |
+
return result.all()
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
async def get_journal(journal_id: uuid.UUID, user_id: uuid.UUID, session: AsyncSession):
|
| 52 |
+
stmt = select(JournalEntry).where(JournalEntry.id == journal_id)
|
| 53 |
+
result = await session.exec(stmt)
|
| 54 |
+
record = result.first()
|
| 55 |
+
|
| 56 |
+
if not record:
|
| 57 |
+
raise ValueError("Journal entry not found")
|
| 58 |
+
if str(record.user_id) != user_id:
|
| 59 |
+
raise ValueError("Not authorized")
|
| 60 |
+
|
| 61 |
+
return record
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
async def update_journal(
|
| 65 |
+
journal_id: uuid.UUID,
|
| 66 |
+
data: JournalUpdate,
|
| 67 |
+
user_id: uuid.UUID,
|
| 68 |
+
session: AsyncSession,
|
| 69 |
+
):
|
| 70 |
+
record = await get_journal(journal_id, user_id, session)
|
| 71 |
+
|
| 72 |
+
if data.title is not None:
|
| 73 |
+
record.title = data.title
|
| 74 |
+
|
| 75 |
+
if data.content is not None:
|
| 76 |
+
record.content = data.content
|
| 77 |
+
|
| 78 |
+
if data.journal_date is not None:
|
| 79 |
+
record.journal_date = data.journal_date
|
| 80 |
+
|
| 81 |
+
record.updated_at = datetime.utcnow()
|
| 82 |
+
|
| 83 |
+
await session.commit()
|
| 84 |
+
await session.refresh(record)
|
| 85 |
+
return record
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
async def delete_journal(journal_id: uuid.UUID, user_id: uuid.UUID, session: AsyncSession):
|
| 89 |
+
record = await get_journal(journal_id, user_id, session)
|
| 90 |
+
await session.delete(record)
|
| 91 |
+
await session.commit()
|
src/main.py
CHANGED
|
@@ -9,6 +9,7 @@ from src.home.router import router as home_router
|
|
| 9 |
from src.notifications.router import router as notifications_router
|
| 10 |
from src.payslip.router import router as payslip_router
|
| 11 |
from src.profile.router import router as profile
|
|
|
|
| 12 |
from fastapi.staticfiles import StaticFiles
|
| 13 |
|
| 14 |
|
|
@@ -36,6 +37,8 @@ app.include_router(notifications_router)
|
|
| 36 |
|
| 37 |
app.include_router(payslip_router)
|
| 38 |
|
|
|
|
|
|
|
| 39 |
|
| 40 |
@app.get("/")
|
| 41 |
def root():
|
|
|
|
| 9 |
from src.notifications.router import router as notifications_router
|
| 10 |
from src.payslip.router import router as payslip_router
|
| 11 |
from src.profile.router import router as profile
|
| 12 |
+
from src.journaling.router import router as journal
|
| 13 |
from fastapi.staticfiles import StaticFiles
|
| 14 |
|
| 15 |
|
|
|
|
| 37 |
|
| 38 |
app.include_router(payslip_router)
|
| 39 |
|
| 40 |
+
app.include_router(journal)
|
| 41 |
+
|
| 42 |
|
| 43 |
@app.get("/")
|
| 44 |
def root():
|