Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion backend/app/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from fastapi import FastAPI
from app.routes import router
from app.db_init import initialize_database
from app.services.db_init import initialize_database
from contextlib import asynccontextmanager
import os

Expand Down
1 change: 0 additions & 1 deletion backend/app/mock_db.py

This file was deleted.

31 changes: 17 additions & 14 deletions backend/app/models.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,25 @@
import time
from typing import Optional

from pydantic import BaseModel
from pydantic import BaseModel, Field
from app.utils import hash_value


class PasteModel(BaseModel):
class PasteInputModel(BaseModel):
"""
Paste model for creating and retrieving pastes
with FastAPI.

Attributes:
content (str): Content of the paste.
workspace (Optional[str]): Optional workspace identifier.
Input model for creating a new paste.
"""

content: str
workspace: Optional[str]
workspace: Optional[str] = ""


class PasteModel(PasteInputModel):
"""
Model for retrieving pastes, including the paste_id.
"""

paste_id: Optional[str]


class PasteDataAware:
Expand All @@ -25,13 +28,13 @@ class PasteDataAware:
with actual database

Attributes:
id (str): Unique identifier for the paste.
paste_id (str): Unique paste_identifier for the paste.
content (str): Content of the paste.
client_id (str): Identifier for the client creating the paste.
client_id (str): paste_identifier for the client creating the paste.
created_at (int): Timestamp of when the paste was created.
"""

id: str
paste_id: str
content: str
client_id: str
created_at: int
Expand All @@ -41,9 +44,9 @@ def __init__(
content: str,
client_id: str,
created_at: Optional[int] = None,
id: str = None,
paste_id: str = None,
):
self.id = hash_value(value=content) if id is None else id
self.paste_id = hash_value(value=content) if paste_id is None else paste_id
self.content = content
self.client_id = client_id
self.created_at = int(time.time()) if created_at is None else created_at
54 changes: 31 additions & 23 deletions backend/app/routes.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,63 @@
from typing import Optional
from fastapi import APIRouter, HTTPException, Request
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Request, status

from app.models import PasteInputModel, PasteModel, PasteDataAware
from app.services.dependencies import get_db_client

from app.models import PasteModel, PasteDataAware
from app.mock_db import mock_db

router = APIRouter()


@router.get("/api/v1/pastes")
async def get_pastes(client_id: Optional[str] = None, request: Request = None):
@router.get("/api/v1/pastes", response_model=List[str])
async def get_pastes(
client_id: Optional[str] = None,
request: Request = None,
db_client=Depends(get_db_client),
):
"""
GET /api/pastes
Retrieve pastes for a specific client.
"""
if not client_id:
client_id = request.client.host # Use client IP as fallback
client_pastes = [
paste for paste in mock_db.values() if paste.get("client_id") == client_id
]
return {"pastes": client_pastes}
pastes = await db_client.get_pastes(client_id)
return [str(paste.paste_id) for paste in pastes]


@router.get("/api/v1/{id}")
async def get_paste(id: str, request: Request):
async def get_paste(id: str, request: Request, db_client=Depends(get_db_client)):
"""
GET /api/v1/{id}
Retrieve a specific paste by ID.
"""
paste = mock_db.get(id)
if not paste:
paste = await db_client.get_paste(id)
if paste is None:
raise HTTPException(status_code=404, detail="Paste not found")
user_agent = request.headers.get("User-Agent", "")
is_web_browser = "Mozilla" in user_agent or "AppleWebKit" in user_agent
return {"paste": paste, "is_web_browser": is_web_browser}

paste_model = PasteModel(
content=paste.content,
paste_id=paste.paste_id,
workspace=paste.client_id,
)

return paste_model

@router.post("/api/v1")
async def create_paste(paste: PasteModel, request: Request):

@router.post("/api/v1", status_code=status.HTTP_201_CREATED)
async def create_paste(
paste: PasteInputModel, request: Request, db_client=Depends(get_db_client)
):
"""
POST /api/v1
Create a new paste.
"""
client_id = paste.workspace if paste.workspace != "" else request.client.host
data = PasteDataAware(content=paste.content, client_id=client_id)
mock_db[data.id] = {
"id": data.id,
"content": data.content,
"client_id": data.client_id,
"created_at": data.created_at,
}
return {"id": data.id}
paste_id = await db_client.insert_paste(data)

return {"id": paste_id}


@router.options("/api/v1")
Expand Down
8 changes: 8 additions & 0 deletions backend/app/scripts/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# app.scripts

## postgresql.py

will setup Postgresql database requirements to have this app work:

- ensures configured schema is created
- ensures configured table is created
50 changes: 17 additions & 33 deletions backend/app/scripts/postgresql.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,17 @@
import asyncpg
import os
from dotenv import load_dotenv


async def create_schema_if_not_exists(conn_details, schema_name):
"""
Create the schema in the database if it doesn't exist.
"""
conn = await asyncpg.connect(**conn_details)

query = f"CREATE SCHEMA IF NOT EXISTS {schema_name};"
await conn.execute(query)
print(f"Schema '{schema_name}' ensured in database '{conn_details['database']}'.")

await conn.close()


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

create_table_query = f"""
CREATE TABLE IF NOT EXISTS {schema_name}.paste (
id TEXT PRIMARY KEY,
id SERIAL PRIMARY KEY, -- Auto-incrementing integer
paste_id UUID DEFAULT gen_random_uuid() UNIQUE NOT NULL, -- Unique GUID
content TEXT NOT NULL,
client_id TEXT NOT NULL,
created_at BIGINT NOT NULL
);
"""
# Ensure the pgcrypto extension is enabled for UUID generation
await conn.execute("CREATE EXTENSION IF NOT EXISTS pgcrypto;")
await conn.execute(create_table_query)
print(
f"Table 'paste' ensured in database {conn_details['database']} under schema {schema_name}."
)

await conn.close()


async def create_schema_if_not_exists(conn_details, schema_name):
conn = await asyncpg.connect(**conn_details)

query = f"CREATE SCHEMA IF NOT EXISTS {schema_name};"
await conn.execute(query)
print(
f"Schema '{schema_name}' ensured in database {conn_details['database']} under schema {schema_name}."
)

await conn.close()


if __name__ == "__main__":
import asyncio

load_dotenv()
conn_details = {
"host": os.getenv("DB_HOST", "localhost"),
"port": os.getenv("DB_PORT", "5432"),
"user": os.getenv("DB_USER"),
"password": os.getenv("DB_PASSWORD"),
"database": os.getenv("DB_NAME"),
}
schema_name = os.getenv("DB_SCHEMA", "public")
asyncio.run(
create_schema_if_not_exists(conn_details=conn_details, schema_name=schema_name)
)
asyncio.run(create_paste_table(conn_details=conn_details, schema_name=schema_name))
Empty file.
105 changes: 105 additions & 0 deletions backend/app/services/db_client.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import asyncpg

from app.models import PasteDataAware


class DBClient:
def __init__(self, conn_details, schema_name="public"):
"""
Initialize the DBClient with connection details and schema name.
"""
self.conn_details = conn_details
self.schema_name = schema_name

async def get_pastes(self, client_id: str) -> list[PasteDataAware]:
"""
Retrieve pastes for a specific client.

Args:
client_id (str): The client ID to filter pastes.

Returns:
list: A list of PasteDataAware objects.
"""
conn = await asyncpg.connect(**self.conn_details)

try:
query = f"""
SELECT content, paste_id, client_id, created_at
FROM {self.schema_name}.paste
WHERE client_id = $1
ORDER BY created_at DESC;
"""
results = await conn.fetch(query, client_id)

return [
PasteDataAware(
content=result["content"],
paste_id=str(result["paste_id"]),
client_id=result["client_id"],
created_at=result["created_at"],
)
for result in results
]
finally:
await conn.close()

async def get_paste(self, id: str) -> PasteDataAware:
"""
Retrieve a paste by its ID.

Args:
id (str): The ID of the paste to retrieve.

Returns:
PasteDataAware: a Paste filled with all its database informations.
"""
conn = await asyncpg.connect(**self.conn_details)

try:
query = f"""
SELECT content, paste_id, client_id, created_at
FROM {self.schema_name}.paste
WHERE paste_id = $1;
"""
result = await conn.fetchrow(query, id)

if result:
return PasteDataAware(
content=result["content"],
paste_id=str(result["paste_id"]),
client_id=result["client_id"],
created_at=result["created_at"],
)
else:
return None
finally:
await conn.close()

async def insert_paste(self, paste: PasteDataAware) -> str:
"""
Insert a new paste into the 'paste' table.

Args:
content (str): The content of the paste.
client_id (str): The client ID associated with the paste.
created_at (int): The timestamp when the paste was created.

Returns:
str: the UUID of the paste.
"""
conn = await asyncpg.connect(**self.conn_details)

try:
insert_query = f"""
INSERT INTO {self.schema_name}.paste (content, client_id, created_at)
VALUES ($1, $2, $3)
RETURNING id, paste_id;
"""
result = await conn.fetchrow(
insert_query, paste.content, paste.client_id, paste.created_at
)

return result["paste_id"]
finally:
await conn.close()
File renamed without changes.
21 changes: 21 additions & 0 deletions backend/app/services/dependencies.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from dotenv import load_dotenv
from app.services.db_client import DBClient
import os


def get_db_client():
"""
Dependency to provide a DBClient instance.
"""

load_dotenv()

conn_details = {
"host": os.getenv("DB_HOST", "localhost"),
"port": os.getenv("DB_PORT", "5432"),
"user": os.getenv("DB_USER"),
"password": os.getenv("DB_PASSWORD"),
"database": os.getenv("DB_NAME"),
}
schema_name = os.getenv("DB_SCHEMA", "public")
return DBClient(conn_details, schema_name)
Loading