Skip to content

Commit 461393b

Browse files
Merge pull request #99 from socraticDevBlog/20250426-readwritedatabase
Refactor database handling and update models for paste management
2 parents ce8b91c + d6b38dd commit 461393b

File tree

11 files changed

+254
-72
lines changed

11 files changed

+254
-72
lines changed

backend/app/main.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from fastapi import FastAPI
22
from app.routes import router
3-
from app.db_init import initialize_database
3+
from app.services.db_init import initialize_database
44
from contextlib import asynccontextmanager
55
import os
66

backend/app/mock_db.py

Lines changed: 0 additions & 1 deletion
This file was deleted.

backend/app/models.py

Lines changed: 17 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import time
22
from typing import Optional
33

4-
from pydantic import BaseModel
4+
from pydantic import BaseModel, Field
55
from app.utils import hash_value
66

77

8-
class PasteModel(BaseModel):
8+
class PasteInputModel(BaseModel):
99
"""
10-
Paste model for creating and retrieving pastes
11-
with FastAPI.
12-
13-
Attributes:
14-
content (str): Content of the paste.
15-
workspace (Optional[str]): Optional workspace identifier.
10+
Input model for creating a new paste.
1611
"""
1712

1813
content: str
19-
workspace: Optional[str]
14+
workspace: Optional[str] = ""
15+
16+
17+
class PasteModel(PasteInputModel):
18+
"""
19+
Model for retrieving pastes, including the paste_id.
20+
"""
21+
22+
paste_id: Optional[str]
2023

2124

2225
class PasteDataAware:
@@ -25,13 +28,13 @@ class PasteDataAware:
2528
with actual database
2629
2730
Attributes:
28-
id (str): Unique identifier for the paste.
31+
paste_id (str): Unique paste_identifier for the paste.
2932
content (str): Content of the paste.
30-
client_id (str): Identifier for the client creating the paste.
33+
client_id (str): paste_identifier for the client creating the paste.
3134
created_at (int): Timestamp of when the paste was created.
3235
"""
3336

34-
id: str
37+
paste_id: str
3538
content: str
3639
client_id: str
3740
created_at: int
@@ -41,9 +44,9 @@ def __init__(
4144
content: str,
4245
client_id: str,
4346
created_at: Optional[int] = None,
44-
id: str = None,
47+
paste_id: str = None,
4548
):
46-
self.id = hash_value(value=content) if id is None else id
49+
self.paste_id = hash_value(value=content) if paste_id is None else paste_id
4750
self.content = content
4851
self.client_id = client_id
4952
self.created_at = int(time.time()) if created_at is None else created_at

backend/app/routes.py

Lines changed: 31 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,63 @@
1-
from typing import Optional
2-
from fastapi import APIRouter, HTTPException, Request
1+
from typing import List, Optional
2+
from fastapi import APIRouter, Depends, HTTPException, Request, status
3+
4+
from app.models import PasteInputModel, PasteModel, PasteDataAware
5+
from app.services.dependencies import get_db_client
36

4-
from app.models import PasteModel, PasteDataAware
5-
from app.mock_db import mock_db
67

78
router = APIRouter()
89

910

10-
@router.get("/api/v1/pastes")
11-
async def get_pastes(client_id: Optional[str] = None, request: Request = None):
11+
@router.get("/api/v1/pastes", response_model=List[str])
12+
async def get_pastes(
13+
client_id: Optional[str] = None,
14+
request: Request = None,
15+
db_client=Depends(get_db_client),
16+
):
1217
"""
1318
GET /api/pastes
1419
Retrieve pastes for a specific client.
1520
"""
1621
if not client_id:
1722
client_id = request.client.host # Use client IP as fallback
18-
client_pastes = [
19-
paste for paste in mock_db.values() if paste.get("client_id") == client_id
20-
]
21-
return {"pastes": client_pastes}
23+
pastes = await db_client.get_pastes(client_id)
24+
return [str(paste.paste_id) for paste in pastes]
2225

2326

2427
@router.get("/api/v1/{id}")
25-
async def get_paste(id: str, request: Request):
28+
async def get_paste(id: str, request: Request, db_client=Depends(get_db_client)):
2629
"""
2730
GET /api/v1/{id}
2831
Retrieve a specific paste by ID.
2932
"""
30-
paste = mock_db.get(id)
31-
if not paste:
33+
paste = await db_client.get_paste(id)
34+
if paste is None:
3235
raise HTTPException(status_code=404, detail="Paste not found")
3336
user_agent = request.headers.get("User-Agent", "")
3437
is_web_browser = "Mozilla" in user_agent or "AppleWebKit" in user_agent
35-
return {"paste": paste, "is_web_browser": is_web_browser}
3638

39+
paste_model = PasteModel(
40+
content=paste.content,
41+
paste_id=paste.paste_id,
42+
workspace=paste.client_id,
43+
)
44+
45+
return paste_model
3746

38-
@router.post("/api/v1")
39-
async def create_paste(paste: PasteModel, request: Request):
47+
48+
@router.post("/api/v1", status_code=status.HTTP_201_CREATED)
49+
async def create_paste(
50+
paste: PasteInputModel, request: Request, db_client=Depends(get_db_client)
51+
):
4052
"""
4153
POST /api/v1
4254
Create a new paste.
4355
"""
4456
client_id = paste.workspace if paste.workspace != "" else request.client.host
4557
data = PasteDataAware(content=paste.content, client_id=client_id)
46-
mock_db[data.id] = {
47-
"id": data.id,
48-
"content": data.content,
49-
"client_id": data.client_id,
50-
"created_at": data.created_at,
51-
}
52-
return {"id": data.id}
58+
paste_id = await db_client.insert_paste(data)
59+
60+
return {"id": paste_id}
5361

5462

5563
@router.options("/api/v1")

backend/app/scripts/README.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# app.scripts
2+
3+
## postgresql.py
4+
5+
will setup Postgresql database requirements to have this app work:
6+
7+
- ensures configured schema is created
8+
- ensures configured table is created

backend/app/scripts/postgresql.py

Lines changed: 17 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,17 @@
11
import asyncpg
2-
import os
3-
from dotenv import load_dotenv
2+
3+
4+
async def create_schema_if_not_exists(conn_details, schema_name):
5+
"""
6+
Create the schema in the database if it doesn't exist.
7+
"""
8+
conn = await asyncpg.connect(**conn_details)
9+
10+
query = f"CREATE SCHEMA IF NOT EXISTS {schema_name};"
11+
await conn.execute(query)
12+
print(f"Schema '{schema_name}' ensured in database '{conn_details['database']}'.")
13+
14+
await conn.close()
415

516

617
async def create_paste_table(conn_details, schema_name):
@@ -11,45 +22,18 @@ async def create_paste_table(conn_details, schema_name):
1122

1223
create_table_query = f"""
1324
CREATE TABLE IF NOT EXISTS {schema_name}.paste (
14-
id TEXT PRIMARY KEY,
25+
id SERIAL PRIMARY KEY, -- Auto-incrementing integer
26+
paste_id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL, -- Unique GUID
1527
content TEXT NOT NULL,
1628
client_id TEXT NOT NULL,
1729
created_at BIGINT NOT NULL
1830
);
1931
"""
32+
# Ensure the pgcrypto extension is enabled for UUID generation
33+
await conn.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto;")
2034
await conn.execute(create_table_query)
2135
print(
2236
f"Table 'paste' ensured in database {conn_details['database']} under schema {schema_name}."
2337
)
2438

2539
await conn.close()
26-
27-
28-
async def create_schema_if_not_exists(conn_details, schema_name):
29-
conn = await asyncpg.connect(**conn_details)
30-
31-
query = f"CREATE SCHEMA IF NOT EXISTS {schema_name};"
32-
await conn.execute(query)
33-
print(
34-
f"Schema '{schema_name}' ensured in database {conn_details['database']} under schema {schema_name}."
35-
)
36-
37-
await conn.close()
38-
39-
40-
if __name__ == "__main__":
41-
import asyncio
42-
43-
load_dotenv()
44-
conn_details = {
45-
"host": os.getenv("DB_HOST", "localhost"),
46-
"port": os.getenv("DB_PORT", "5432"),
47-
"user": os.getenv("DB_USER"),
48-
"password": os.getenv("DB_PASSWORD"),
49-
"database": os.getenv("DB_NAME"),
50-
}
51-
schema_name = os.getenv("DB_SCHEMA", "public")
52-
asyncio.run(
53-
create_schema_if_not_exists(conn_details=conn_details, schema_name=schema_name)
54-
)
55-
asyncio.run(create_paste_table(conn_details=conn_details, schema_name=schema_name))

backend/app/services/__init__.py

Whitespace-only changes.

backend/app/services/db_client.py

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import asyncpg
2+
3+
from app.models import PasteDataAware
4+
5+
6+
class DBClient:
7+
def __init__(self, conn_details, schema_name="public"):
8+
"""
9+
Initialize the DBClient with connection details and schema name.
10+
"""
11+
self.conn_details = conn_details
12+
self.schema_name = schema_name
13+
14+
async def get_pastes(self, client_id: str) -> list[PasteDataAware]:
15+
"""
16+
Retrieve pastes for a specific client.
17+
18+
Args:
19+
client_id (str): The client ID to filter pastes.
20+
21+
Returns:
22+
list: A list of PasteDataAware objects.
23+
"""
24+
conn = await asyncpg.connect(**self.conn_details)
25+
26+
try:
27+
query = f"""
28+
SELECT content, paste_id, client_id, created_at
29+
FROM {self.schema_name}.paste
30+
WHERE client_id = $1
31+
ORDER BY created_at DESC;
32+
"""
33+
results = await conn.fetch(query, client_id)
34+
35+
return [
36+
PasteDataAware(
37+
content=result["content"],
38+
paste_id=str(result["paste_id"]),
39+
client_id=result["client_id"],
40+
created_at=result["created_at"],
41+
)
42+
for result in results
43+
]
44+
finally:
45+
await conn.close()
46+
47+
async def get_paste(self, id: str) -> PasteDataAware:
48+
"""
49+
Retrieve a paste by its ID.
50+
51+
Args:
52+
id (str): The ID of the paste to retrieve.
53+
54+
Returns:
55+
PasteDataAware: a Paste filled with all its database informations.
56+
"""
57+
conn = await asyncpg.connect(**self.conn_details)
58+
59+
try:
60+
query = f"""
61+
SELECT content, paste_id, client_id, created_at
62+
FROM {self.schema_name}.paste
63+
WHERE paste_id = $1;
64+
"""
65+
result = await conn.fetchrow(query, id)
66+
67+
if result:
68+
return PasteDataAware(
69+
content=result["content"],
70+
paste_id=str(result["paste_id"]),
71+
client_id=result["client_id"],
72+
created_at=result["created_at"],
73+
)
74+
else:
75+
return None
76+
finally:
77+
await conn.close()
78+
79+
async def insert_paste(self, paste: PasteDataAware) -> str:
80+
"""
81+
Insert a new paste into the 'paste' table.
82+
83+
Args:
84+
content (str): The content of the paste.
85+
client_id (str): The client ID associated with the paste.
86+
created_at (int): The timestamp when the paste was created.
87+
88+
Returns:
89+
str: the UUID of the paste.
90+
"""
91+
conn = await asyncpg.connect(**self.conn_details)
92+
93+
try:
94+
insert_query = f"""
95+
INSERT INTO {self.schema_name}.paste (content, client_id, created_at)
96+
VALUES ($1, $2, $3)
97+
RETURNING id, paste_id;
98+
"""
99+
result = await conn.fetchrow(
100+
insert_query, paste.content, paste.client_id, paste.created_at
101+
)
102+
103+
return result["paste_id"]
104+
finally:
105+
await conn.close()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
from dotenv import load_dotenv
2+
from app.services.db_client import DBClient
3+
import os
4+
5+
6+
def get_db_client():
7+
"""
8+
Dependency to provide a DBClient instance.
9+
"""
10+
11+
load_dotenv()
12+
13+
conn_details = {
14+
"host": os.getenv("DB_HOST", "localhost"),
15+
"port": os.getenv("DB_PORT", "5432"),
16+
"user": os.getenv("DB_USER"),
17+
"password": os.getenv("DB_PASSWORD"),
18+
"database": os.getenv("DB_NAME"),
19+
}
20+
schema_name = os.getenv("DB_SCHEMA", "public")
21+
return DBClient(conn_details, schema_name)

0 commit comments

Comments
 (0)