first commit

This commit is contained in:
2025-11-24 23:10:55 -06:00
commit 8315fa51c9
279 changed files with 74600 additions and 0 deletions

View File

@@ -0,0 +1,441 @@
"""
Database Service for Appwrite database operations.
This service wraps the Appwrite Databases SDK to provide a clean interface
for CRUD operations on collections. It handles JSON serialization, error handling,
and provides structured logging.
"""
import os
from typing import Dict, Any, List, Optional
from datetime import datetime, timezone
from dataclasses import dataclass
from appwrite.client import Client
from appwrite.services.tables_db import TablesDB
from appwrite.exception import AppwriteException
from appwrite.id import ID
from app.utils.logging import get_logger
logger = get_logger(__file__)
@dataclass
class DatabaseRow:
"""
Represents a row in an Appwrite table.
Attributes:
id: Row ID
table_id: Table ID
data: Row data (parsed from JSON)
created_at: Creation timestamp
updated_at: Last update timestamp
"""
id: str
table_id: str
data: Dict[str, Any]
created_at: datetime
updated_at: datetime
def to_dict(self) -> Dict[str, Any]:
"""Convert row to dictionary."""
return {
"id": self.id,
"table_id": self.table_id,
"data": self.data,
"created_at": self.created_at.isoformat() if isinstance(self.created_at, datetime) else self.created_at,
"updated_at": self.updated_at.isoformat() if isinstance(self.updated_at, datetime) else self.updated_at,
}
class DatabaseService:
"""
Service for interacting with Appwrite database tables.
This service provides methods for:
- Creating rows
- Reading rows by ID or query
- Updating rows
- Deleting rows
- Querying with filters
"""
def __init__(self):
"""
Initialize the database service.
Reads configuration from environment variables:
- APPWRITE_ENDPOINT: Appwrite API endpoint
- APPWRITE_PROJECT_ID: Appwrite project ID
- APPWRITE_API_KEY: Appwrite API key
- APPWRITE_DATABASE_ID: Appwrite database ID
"""
self.endpoint = os.getenv('APPWRITE_ENDPOINT')
self.project_id = os.getenv('APPWRITE_PROJECT_ID')
self.api_key = os.getenv('APPWRITE_API_KEY')
self.database_id = os.getenv('APPWRITE_DATABASE_ID', 'main')
if not all([self.endpoint, self.project_id, self.api_key]):
logger.error("Missing Appwrite configuration in environment variables")
raise ValueError("Appwrite configuration incomplete. Check APPWRITE_* environment variables.")
# Initialize Appwrite client
self.client = Client()
self.client.set_endpoint(self.endpoint)
self.client.set_project(self.project_id)
self.client.set_key(self.api_key)
# Initialize TablesDB service
self.tables_db = TablesDB(self.client)
logger.info("DatabaseService initialized", database_id=self.database_id)
def create_row(
self,
table_id: str,
data: Dict[str, Any],
row_id: Optional[str] = None,
permissions: Optional[List[str]] = None
) -> DatabaseRow:
"""
Create a new row in a table.
Args:
table_id: Table ID (e.g., "characters")
data: Row data (will be JSON-serialized if needed)
row_id: Optional custom row ID (auto-generated if None)
permissions: Optional permissions array
Returns:
DatabaseRow with created row
Raises:
AppwriteException: If creation fails
"""
try:
logger.info("Creating row", table_id=table_id, has_custom_id=bool(row_id))
# Generate ID if not provided
if row_id is None:
row_id = ID.unique()
# Create row (Appwrite manages timestamps automatically via $createdAt/$updatedAt)
result = self.tables_db.create_row(
database_id=self.database_id,
table_id=table_id,
row_id=row_id,
data=data,
permissions=permissions or []
)
logger.info("Row created successfully",
table_id=table_id,
row_id=result['$id'])
return self._parse_row(result, table_id)
except AppwriteException as e:
logger.error("Failed to create row",
table_id=table_id,
error=str(e),
code=e.code)
raise
def get_row(self, table_id: str, row_id: str) -> Optional[DatabaseRow]:
"""
Get a row by ID.
Args:
table_id: Table ID
row_id: Row ID
Returns:
DatabaseRow or None if not found
Raises:
AppwriteException: If retrieval fails (except 404)
"""
try:
logger.debug("Fetching row", table_id=table_id, row_id=row_id)
result = self.tables_db.get_row(
database_id=self.database_id,
table_id=table_id,
row_id=row_id
)
return self._parse_row(result, table_id)
except AppwriteException as e:
if e.code == 404:
logger.warning("Row not found",
table_id=table_id,
row_id=row_id)
return None
logger.error("Failed to fetch row",
table_id=table_id,
row_id=row_id,
error=str(e),
code=e.code)
raise
def update_row(
self,
table_id: str,
row_id: str,
data: Dict[str, Any],
permissions: Optional[List[str]] = None
) -> DatabaseRow:
"""
Update an existing row.
Args:
table_id: Table ID
row_id: Row ID
data: New row data (partial updates supported)
permissions: Optional permissions array
Returns:
DatabaseRow with updated row
Raises:
AppwriteException: If update fails
"""
try:
logger.info("Updating row", table_id=table_id, row_id=row_id)
# Update row (Appwrite manages timestamps automatically via $updatedAt)
result = self.tables_db.update_row(
database_id=self.database_id,
table_id=table_id,
row_id=row_id,
data=data,
permissions=permissions
)
logger.info("Row updated successfully",
table_id=table_id,
row_id=row_id)
return self._parse_row(result, table_id)
except AppwriteException as e:
logger.error("Failed to update row",
table_id=table_id,
row_id=row_id,
error=str(e),
code=e.code)
raise
def delete_row(self, table_id: str, row_id: str) -> bool:
"""
Delete a row.
Args:
table_id: Table ID
row_id: Row ID
Returns:
True if deletion successful
Raises:
AppwriteException: If deletion fails
"""
try:
logger.info("Deleting row", table_id=table_id, row_id=row_id)
self.tables_db.delete_row(
database_id=self.database_id,
table_id=table_id,
row_id=row_id
)
logger.info("Row deleted successfully",
table_id=table_id,
row_id=row_id)
return True
except AppwriteException as e:
logger.error("Failed to delete row",
table_id=table_id,
row_id=row_id,
error=str(e),
code=e.code)
raise
def list_rows(
self,
table_id: str,
queries: Optional[List[str]] = None,
limit: int = 25,
offset: int = 0
) -> List[DatabaseRow]:
"""
List rows in a table with optional filtering.
Args:
table_id: Table ID
queries: Optional Appwrite query filters
limit: Maximum rows to return (default 25, max 100)
offset: Number of rows to skip
Returns:
List of DatabaseRow instances
Raises:
AppwriteException: If query fails
"""
try:
logger.debug("Listing rows",
table_id=table_id,
has_queries=bool(queries),
limit=limit,
offset=offset)
result = self.tables_db.list_rows(
database_id=self.database_id,
table_id=table_id,
queries=queries or []
)
rows = [self._parse_row(row, table_id) for row in result['rows']]
logger.debug("Rows listed successfully",
table_id=table_id,
count=len(rows),
total=result.get('total', len(rows)))
return rows
except AppwriteException as e:
logger.error("Failed to list rows",
table_id=table_id,
error=str(e),
code=e.code)
raise
def count_rows(self, table_id: str, queries: Optional[List[str]] = None) -> int:
"""
Count rows in a table with optional filtering.
Args:
table_id: Table ID
queries: Optional Appwrite query filters
Returns:
Row count
Raises:
AppwriteException: If query fails
"""
try:
logger.debug("Counting rows", table_id=table_id, has_queries=bool(queries))
result = self.tables_db.list_rows(
database_id=self.database_id,
table_id=table_id,
queries=queries or []
)
count = result.get('total', len(result.get('rows', [])))
logger.debug("Rows counted", table_id=table_id, count=count)
return count
except AppwriteException as e:
logger.error("Failed to count rows",
table_id=table_id,
error=str(e),
code=e.code)
raise
def _parse_row(self, row: Dict[str, Any], table_id: str) -> DatabaseRow:
"""
Parse Appwrite row into DatabaseRow.
Args:
row: Appwrite row dictionary
table_id: Table ID
Returns:
DatabaseRow instance
"""
# Extract metadata
row_id = row['$id']
created_at = row.get('$createdAt', datetime.now(timezone.utc).isoformat())
updated_at = row.get('$updatedAt', datetime.now(timezone.utc).isoformat())
# Parse timestamps
if isinstance(created_at, str):
created_at = datetime.fromisoformat(created_at.replace('Z', '+00:00'))
if isinstance(updated_at, str):
updated_at = datetime.fromisoformat(updated_at.replace('Z', '+00:00'))
# Remove Appwrite metadata from data
data = {k: v for k, v in row.items() if not k.startswith('$')}
return DatabaseRow(
id=row_id,
table_id=table_id,
data=data,
created_at=created_at,
updated_at=updated_at
)
# Backward compatibility aliases (deprecated, use new methods)
def create_document(self, collection_id: str, data: Dict[str, Any],
document_id: Optional[str] = None,
permissions: Optional[List[str]] = None) -> DatabaseRow:
"""Deprecated: Use create_row() instead."""
logger.warning("create_document() is deprecated, use create_row() instead")
return self.create_row(collection_id, data, document_id, permissions)
def get_document(self, collection_id: str, document_id: str) -> Optional[DatabaseRow]:
"""Deprecated: Use get_row() instead."""
logger.warning("get_document() is deprecated, use get_row() instead")
return self.get_row(collection_id, document_id)
def update_document(self, collection_id: str, document_id: str,
data: Dict[str, Any],
permissions: Optional[List[str]] = None) -> DatabaseRow:
"""Deprecated: Use update_row() instead."""
logger.warning("update_document() is deprecated, use update_row() instead")
return self.update_row(collection_id, document_id, data, permissions)
def delete_document(self, collection_id: str, document_id: str) -> bool:
"""Deprecated: Use delete_row() instead."""
logger.warning("delete_document() is deprecated, use delete_row() instead")
return self.delete_row(collection_id, document_id)
def list_documents(self, collection_id: str, queries: Optional[List[str]] = None,
limit: int = 25, offset: int = 0) -> List[DatabaseRow]:
"""Deprecated: Use list_rows() instead."""
logger.warning("list_documents() is deprecated, use list_rows() instead")
return self.list_rows(collection_id, queries, limit, offset)
def count_documents(self, collection_id: str, queries: Optional[List[str]] = None) -> int:
"""Deprecated: Use count_rows() instead."""
logger.warning("count_documents() is deprecated, use count_rows() instead")
return self.count_rows(collection_id, queries)
# Backward compatibility alias
DatabaseDocument = DatabaseRow
# Global instance for convenience
_service_instance: Optional[DatabaseService] = None
def get_database_service() -> DatabaseService:
"""
Get the global DatabaseService instance.
Returns:
Singleton DatabaseService instance
"""
global _service_instance
if _service_instance is None:
_service_instance = DatabaseService()
return _service_instance