Skip to content
Open
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
104 changes: 103 additions & 1 deletion src/cocat/app/app.py
Original file line number Diff line number Diff line change
@@ -1,22 +1,31 @@
import os
from contextlib import asynccontextmanager
from datetime import datetime
from functools import partial
from pathlib import Path
from typing import Any

from anyio import sleep_forever
from anyio.abc import TaskStatus
from fastapi import Depends, FastAPI, WebSocket, WebSocketDisconnect
from fastapi import Depends, FastAPI, HTTPException, WebSocket, WebSocketDisconnect
from fastapi.middleware.cors import CORSMiddleware
from sqlalchemy import select
from wire_file import AsyncFileClient
from wiredb import AsyncChannel, Room, RoomManager

from ..db import DB as CrdtDB
from .db import User, get_db
from .schemas import RoomUsers, UserCreate, UserRead, UserRooms, UserUpdate
from .users import get_backend


def _dt_to_iso(dt: Any) -> str:
"""Return ISO 8601 timestamp without timezone suffix."""
if isinstance(dt, datetime):
return dt.strftime("%Y-%m-%dT%H:%M:%S")
return str(dt).replace("Z", "").split("+")[0]


class StoredRoom(Room): # pragma: nocover
def __init__(self, directory: str, *args: Any, **kwargs: Any):
super().__init__(*args, **kwargs)
Expand Down Expand Up @@ -120,6 +129,99 @@ async def get_room_users(room_id: str):
"users": [usr.User.email for usr in users if room_id in usr.User.rooms]
}

@app.get("/rooms/{room_id}/catalogues")
async def get_room_catalogues(
room_id: str,
user: User = Depends(backend.current_active_user),
):
if room_id not in user.rooms:
raise HTTPException(status_code=403, detail="Not authorized")
room = await self.room_manager.get_room(room_id)
crdt_db = CrdtDB(doc=room.doc)
return {
"catalogues": [
{
"uuid": str(cat._uuid),
"name": cat.name,
"nb_events": len(cat.events),
"author": cat._map.get("author", ""),
"tags": list(
cat._map.get("tags", {}).keys()
if hasattr(cat._map.get("tags", None), "keys")
else []
),
"attributes": dict(cat._map.get("attributes", {}) or {}),
}
for cat in crdt_db.catalogues
]
}

@app.get("/catalogues/{uuid}")
async def get_catalogue(
uuid: str,
user: User = Depends(backend.current_active_user),
):
for room_id in user.rooms:
room = await self.room_manager.get_room(room_id)
crdt_db = CrdtDB(doc=room.doc)
for cat in crdt_db.catalogues:
if str(cat._uuid) == uuid:
return {
"uuid": uuid,
"room_id": room_id,
"name": cat.name,
"nb_events": len(cat.events),
"author": cat._map.get("author", ""),
"tags": list(
cat._map.get("tags", {}).keys()
if hasattr(cat._map.get("tags", None), "keys")
else []
),
"attributes": dict(cat._map.get("attributes", {}) or {}),
}
raise HTTPException(status_code=404, detail="Catalogue not found")

@app.get("/catalogues/{uuid}/events")
async def get_catalogue_events(
uuid: str,
user: User = Depends(backend.current_active_user),
):
for room_id in user.rooms:
room = await self.room_manager.get_room(room_id)
crdt_db = CrdtDB(doc=room.doc)
for cat in crdt_db.catalogues:
if str(cat._uuid) == uuid:
events = sorted(cat.events, key=lambda e: e.start)
return {
"uuid": uuid,
"name": cat.name,
"room_id": room_id,
"author": cat._map.get("author", ""),
"tags": list(
cat._map.get("tags", {}).keys()
if hasattr(cat._map.get("tags", None), "keys")
else []
),
"events": [
{
"uuid": str(e._uuid),
"start": _dt_to_iso(e.start),
"stop": _dt_to_iso(e.stop),
"author": e._map.get("author", ""),
"tags": list(
e._map.get("tags", {}).keys()
if hasattr(e._map.get("tags", None), "keys")
else []
),
"attributes": dict(
e._map.get("attributes", {}) or {}
),
}
for e in events
],
}
raise HTTPException(status_code=404, detail="Catalogue not found")


class YWebSocket(AsyncChannel): # pragma: nocover
def __init__(self, websocket: WebSocket, path: str) -> None:
Expand Down
123 changes: 123 additions & 0 deletions tests/test_rest_catalogues.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
from uuid import uuid4

import httpx
import pytest
from anyio import sleep
from wire_websocket import AsyncWebSocketClient

from cocat import DB

pytestmark = pytest.mark.anyio


async def test_room_catalogues(server, user, room_id):
host, port = server
username, password = user
data = {"username": username, "password": password}
response = httpx.post(f"http://{host}:{port}/auth/jwt/login", data=data)
cookie = response.cookies.get("fastapiusersauth")
cookies = httpx.Cookies()
cookies.set("fastapiusersauth", cookie)

# unauthenticated
response = httpx.get(f"http://{host}:{port}/rooms/{room_id}/catalogues")
assert response.status_code == 401

# room not accessible by user
response = httpx.get(
f"http://{host}:{port}/rooms/{uuid4()}/catalogues", cookies=cookies
)
assert response.status_code == 403

# empty room
response = httpx.get(
f"http://{host}:{port}/rooms/{room_id}/catalogues", cookies=cookies
)
assert response.status_code == 200
assert response.json() == {"catalogues": []}

# create catalogue with two events via WebSocket
async with AsyncWebSocketClient(
id=f"room/{room_id}",
host=f"http://{host}",
port=port,
cookies=cookies,
) as client:
db = DB(doc=client.doc)
cat = db.create_catalogue(name="Solar Wind Events", author="tester")
cat_uuid = str(cat._uuid)
e1 = db.create_event(start="2024-03-15", stop="2024-03-16", author="tester")
e2 = db.create_event(start="2024-01-10", stop="2024-01-11", author="tester")
cat.add_events(e1)
cat.add_events(e2)
await sleep(0.2)

response = httpx.get(
f"http://{host}:{port}/rooms/{room_id}/catalogues", cookies=cookies
)
assert response.status_code == 200
catalogues = response.json()["catalogues"]
assert len(catalogues) == 1
assert catalogues[0]["uuid"] == cat_uuid
assert catalogues[0]["name"] == "Solar Wind Events"
assert catalogues[0]["nb_events"] == 2


async def test_catalogue_and_events(server, user, room_id):
host, port = server
username, password = user
data = {"username": username, "password": password}
response = httpx.post(f"http://{host}:{port}/auth/jwt/login", data=data)
cookie = response.cookies.get("fastapiusersauth")
cookies = httpx.Cookies()
cookies.set("fastapiusersauth", cookie)

# not found
fake_uuid = str(uuid4())
assert (
httpx.get(
f"http://{host}:{port}/catalogues/{fake_uuid}", cookies=cookies
).status_code
== 404
)
assert (
httpx.get(
f"http://{host}:{port}/catalogues/{fake_uuid}/events", cookies=cookies
).status_code
== 404
)

# create data
async with AsyncWebSocketClient(
id=f"room/{room_id}",
host=f"http://{host}",
port=port,
cookies=cookies,
) as client:
db = DB(doc=client.doc)
cat = db.create_catalogue(name="Solar Wind Events", author="tester")
cat_uuid = str(cat._uuid)
e1 = db.create_event(start="2024-03-15", stop="2024-03-16", author="tester")
e2 = db.create_event(start="2024-01-10", stop="2024-01-11", author="tester")
cat.add_events(e1)
cat.add_events(e2)
await sleep(0.2)

# GET /catalogues/{uuid}
response = httpx.get(f"http://{host}:{port}/catalogues/{cat_uuid}", cookies=cookies)
assert response.status_code == 200
data = response.json()
assert data["uuid"] == cat_uuid
assert data["name"] == "Solar Wind Events"
assert data["room_id"] == room_id
assert data["nb_events"] == 2

# GET /catalogues/{uuid}/events — sorted by start
response = httpx.get(
f"http://{host}:{port}/catalogues/{cat_uuid}/events", cookies=cookies
)
assert response.status_code == 200
events = response.json()["events"]
assert len(events) == 2
assert events[0]["start"] == "2024-01-10T00:00:00"
assert events[1]["start"] == "2024-03-15T00:00:00"
Loading