feat(adapter): implement JSON file storage adapter
Add JsonFileRoomStorage and JsonFileTrackingStorage implementations that implement the RoomStorage and TrackingStorage ports. - Stores room data at ~/.jigaido/data/<room_id>.json - Stores tracking data at ~/.jigaido/tracking/<room_id>_<user_id>.json - Implements all port methods: load, save, add_bounty, update_bounty, delete_bounty, get_bounty for rooms; load, save, track_bounty, untrack_bounty for tracking Fixes #9
This commit is contained in:
0
adapters/__init__.py
Normal file
0
adapters/__init__.py
Normal file
0
adapters/storage/__init__.py
Normal file
0
adapters/storage/__init__.py
Normal file
202
adapters/storage/json_file.py
Normal file
202
adapters/storage/json_file.py
Normal file
@@ -0,0 +1,202 @@
|
||||
"""JSON file storage adapter for JIGAIDO.
|
||||
|
||||
Implements RoomStorage and TrackingStorage ports using JSON file persistence.
|
||||
Data stored at:
|
||||
- Rooms: ~/.jigaido/data/<room_id>.json
|
||||
- Tracking: ~/.jigaido/tracking/<room_id>_<user_id>.json
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
from core.models import Bounty, RoomData, TrackingData, TrackedBounty
|
||||
from core.ports import RoomStorage, TrackingStorage
|
||||
|
||||
|
||||
class JsonFileRoomStorage:
|
||||
"""RoomStorage implementation using JSON files.
|
||||
|
||||
Stores room data at ~/.jigaido/data/<room_id>.json
|
||||
"""
|
||||
|
||||
def __init__(self, data_dir: Path | None = None):
|
||||
if data_dir is None:
|
||||
data_dir = Path.home() / ".jigaido" / "data"
|
||||
self._data_dir = data_dir
|
||||
self._data_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_file_path(self, room_id: int) -> Path:
|
||||
return self._data_dir / f"{room_id}.json"
|
||||
|
||||
def load(self, room_id: int) -> RoomData | None:
|
||||
"""Load room data from JSON file. Returns None if not found."""
|
||||
file_path = self._get_file_path(room_id)
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
bounties = [
|
||||
Bounty(
|
||||
id=b["id"],
|
||||
text=b.get("text"),
|
||||
link=b.get("link"),
|
||||
due_date_ts=b.get("due_date_ts"),
|
||||
created_at=b["created_at"],
|
||||
created_by_user_id=b["created_by_user_id"],
|
||||
)
|
||||
for b in data.get("bounties", [])
|
||||
]
|
||||
|
||||
return RoomData(
|
||||
room_id=data["room_id"],
|
||||
bounties=bounties,
|
||||
next_id=data["next_id"],
|
||||
)
|
||||
|
||||
def save(self, room_data: RoomData) -> None:
|
||||
"""Save room data to JSON file."""
|
||||
data = {
|
||||
"room_id": room_data.room_id,
|
||||
"next_id": room_data.next_id,
|
||||
"bounties": [
|
||||
{
|
||||
"id": b.id,
|
||||
"text": b.text,
|
||||
"link": b.link,
|
||||
"due_date_ts": b.due_date_ts,
|
||||
"created_at": b.created_at,
|
||||
"created_by_user_id": b.created_by_user_id,
|
||||
}
|
||||
for b in room_data.bounties
|
||||
],
|
||||
}
|
||||
|
||||
with open(self._get_file_path(room_data.room_id), "w") as f:
|
||||
json.dump(data, f)
|
||||
|
||||
def add_bounty(self, room_id: int, bounty: Bounty) -> None:
|
||||
"""Add a bounty to a room, creating the room if necessary."""
|
||||
room_data = self.load(room_id)
|
||||
if room_data is None:
|
||||
room_data = RoomData(room_id=room_id, bounties=[], next_id=1)
|
||||
|
||||
room_data.bounties.append(bounty)
|
||||
if bounty.id >= room_data.next_id:
|
||||
room_data.next_id = bounty.id + 1
|
||||
|
||||
self.save(room_data)
|
||||
|
||||
def update_bounty(self, room_id: int, bounty: Bounty) -> None:
|
||||
"""Update an existing bounty in a room."""
|
||||
room_data = self.load(room_id)
|
||||
if room_data is None:
|
||||
return
|
||||
|
||||
for i, b in enumerate(room_data.bounties):
|
||||
if b.id == bounty.id:
|
||||
room_data.bounties[i] = bounty
|
||||
break
|
||||
|
||||
self.save(room_data)
|
||||
|
||||
def delete_bounty(self, room_id: int, bounty_id: int) -> None:
|
||||
"""Delete a bounty from a room."""
|
||||
room_data = self.load(room_id)
|
||||
if room_data is None:
|
||||
return
|
||||
|
||||
room_data.bounties = [b for b in room_data.bounties if b.id != bounty_id]
|
||||
self.save(room_data)
|
||||
|
||||
def get_bounty(self, room_id: int, bounty_id: int) -> Bounty | None:
|
||||
"""Get a specific bounty from a room by ID."""
|
||||
room_data = self.load(room_id)
|
||||
if room_data is None:
|
||||
return None
|
||||
|
||||
for b in room_data.bounties:
|
||||
if b.id == bounty_id:
|
||||
return b
|
||||
|
||||
return None
|
||||
|
||||
|
||||
class JsonFileTrackingStorage:
|
||||
"""TrackingStorage implementation using JSON files.
|
||||
|
||||
Stores tracking data at ~/.jigaido/tracking/<room_id>_<user_id>.json
|
||||
"""
|
||||
|
||||
def __init__(self, tracking_dir: Path | None = None):
|
||||
if tracking_dir is None:
|
||||
tracking_dir = Path.home() / ".jigaido" / "tracking"
|
||||
self._tracking_dir = tracking_dir
|
||||
self._tracking_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _get_file_path(self, room_id: int, user_id: int) -> Path:
|
||||
return self._tracking_dir / f"{room_id}_{user_id}.json"
|
||||
|
||||
def load(self, room_id: int, user_id: int) -> TrackingData | None:
|
||||
"""Load tracking data from JSON file. Returns None if not found."""
|
||||
file_path = self._get_file_path(room_id, user_id)
|
||||
if not file_path.exists():
|
||||
return None
|
||||
|
||||
with open(file_path, "r") as f:
|
||||
data = json.load(f)
|
||||
|
||||
tracked = [
|
||||
TrackedBounty(
|
||||
bounty_id=t["bounty_id"],
|
||||
created_at=t["created_at"],
|
||||
)
|
||||
for t in data.get("tracked", [])
|
||||
]
|
||||
|
||||
return TrackingData(
|
||||
room_id=data["room_id"],
|
||||
user_id=data["user_id"],
|
||||
tracked=tracked,
|
||||
)
|
||||
|
||||
def save(self, tracking_data: TrackingData) -> None:
|
||||
"""Save tracking data to JSON file."""
|
||||
data = {
|
||||
"room_id": tracking_data.room_id,
|
||||
"user_id": tracking_data.user_id,
|
||||
"tracked": [
|
||||
{
|
||||
"bounty_id": t.bounty_id,
|
||||
"created_at": t.created_at,
|
||||
}
|
||||
for t in tracking_data.tracked
|
||||
],
|
||||
}
|
||||
|
||||
with open(
|
||||
self._get_file_path(tracking_data.room_id, tracking_data.user_id), "w"
|
||||
) as f:
|
||||
json.dump(data, f)
|
||||
|
||||
def track_bounty(self, room_id: int, user_id: int, tracked: TrackedBounty) -> None:
|
||||
"""Add a bounty to a user's tracking list, creating the tracking entry if needed."""
|
||||
tracking_data = self.load(room_id, user_id)
|
||||
if tracking_data is None:
|
||||
tracking_data = TrackingData(room_id=room_id, user_id=user_id, tracked=[])
|
||||
|
||||
tracking_data.tracked.append(tracked)
|
||||
self.save(tracking_data)
|
||||
|
||||
def untrack_bounty(self, room_id: int, user_id: int, bounty_id: int) -> None:
|
||||
"""Remove a bounty from a user's tracking list."""
|
||||
tracking_data = self.load(room_id, user_id)
|
||||
if tracking_data is None:
|
||||
return
|
||||
|
||||
tracking_data.tracked = [
|
||||
t for t in tracking_data.tracked if t.bounty_id != bounty_id
|
||||
]
|
||||
self.save(tracking_data)
|
||||
Reference in New Issue
Block a user