diff --git a/CCS_Detailed_Design.md b/CCS_Detailed_Design.md new file mode 100644 index 0000000000..5b653c9ac9 --- /dev/null +++ b/CCS_Detailed_Design.md @@ -0,0 +1,206 @@ +# Central Communication Server (CCS) - Detailed Design + +## 1. Introduction + +This document outlines the detailed design for the Central Communication Server (CCS). The CCS is a core component responsible for managing real-time communication, user authentication, presence, and message persistence. + +## 2. Technology Stack + +* **Programming Language:** Python 3.9+ +* **Web Framework:** FastAPI +* **Asynchronous Server Gateway Interface (ASGI):** Uvicorn +* **Real-time Communication Protocol:** WebSockets +* **Database:** PostgreSQL +* **Authentication:** JSON Web Tokens (JWT) +* **Caching (Optional, for future scalability):** Redis (e.g., for presence status, session management) + +## 3. System Architecture Overview + +The CCS will expose WebSocket endpoints for real-time communication and HTTP endpoints for authentication and user management. It will interact with the PostgreSQL database for persistent storage. + +``` ++-------------------+ +------------------------+ +-------------------+ +| Clients |<---->| CCS |<---->| PostgreSQL | +| (Web, Mobile, CLI)| | (FastAPI, WebSockets) | | Database | ++-------------------+ +------------------------+ +-------------------+ + | + | (Future Enhancement) + | + v + +-------+ + | Redis | + +-------+ +``` + +## 4. Module Structure + +The CCS will be organized into the following primary modules: + +* **`main.py`**: Entry point of the application. Initializes FastAPI app, database connections, and routes. +* **`auth/`**: Handles user authentication and JWT management. + * `auth_service.py`: Logic for user registration, login, password hashing, JWT generation and validation. + * `auth_routes.py`: HTTP API endpoints for `/register`, `/login`. +* **`users/`**: Manages user profiles and presence. + * `user_models.py`: Pydantic models for user data. + * `user_service.py`: Logic for fetching user details, updating profiles. + * `presence_service.py`: Manages user online/offline status and broadcasts updates. +* **`messaging/`**: Handles real-time message routing and persistence. + * `connection_manager.py`: Manages active WebSocket connections. Stores connections per user or per group. + * `message_router.py`: Routes incoming messages to appropriate recipients (one-to-one, group). + * `message_service.py`: Handles storage and retrieval of messages from the database. + * `websocket_routes.py`: WebSocket endpoints for establishing connections and message exchange. +* **`database/`**: Manages database interactions. + * `db_config.py`: Database connection settings. + * `db_models.py`: SQLAlchemy ORM models for database tables. + * `crud.py`: Create, Read, Update, Delete operations for database models. +* **`core/`**: Core utilities and configurations. + * `config.py`: Application settings (e.g., JWT secret, database URL). + * `security.py`: Password hashing utilities. +* **`tests/`**: Unit and integration tests for all modules. + +## 5. Key Classes and Functions + +### 5.1. `auth/auth_service.py` + +* `class AuthService`: + * `async def register_user(user_data: UserCreateSchema) -> UserSchema`: Registers a new user. Hashes password. Stores in DB. + * `async def authenticate_user(username: str, password: str) -> Optional[UserSchema]`: Authenticates a user. + * `def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str`: Creates a JWT. + * `async def get_current_user(token: str = Depends(oauth2_scheme)) -> UserSchema`: Decodes JWT and retrieves user. + +### 5.2. `users/presence_service.py` + +* `class PresenceService`: + * `active_users: set[str] = set()`: Stores user IDs of currently online users. + * `async def user_connected(user_id: str)`: Marks user as online, broadcasts presence. + * `async def user_disconnected(user_id: str)`: Marks user as offline, broadcasts presence. + * `async def broadcast_presence_update(user_id: str, status: str)` + +### 5.3. `messaging/connection_manager.py` + +* `class ConnectionManager`: + * `active_connections: dict[str, WebSocket] = {}`: Maps user_id to WebSocket connection. + * `async def connect(user_id: str, websocket: WebSocket)`: Accepts and stores a new connection. + * `def disconnect(user_id: str)`: Removes a connection. + * `async def send_personal_message(message: str, user_id: str)` + * `async def broadcast(message: str)`: Sends a message to all connected clients (e.g., for system-wide announcements or group chats if not handled separately). + * `async def send_to_group(group_id: str, message: str)`: (If group chat is implemented) Sends message to all members of a group. + +### 5.4. `messaging/message_router.py` + +* `class MessageRouter`: + * `def __init__(self, connection_manager: ConnectionManager, message_service: MessageService)` + * `async def route_message(sender_id: str, raw_message: dict)`: Parses message type (e.g., one-to-one, group, system), validates, and forwards to `ConnectionManager` or `MessageService`. + +### 5.5. `messaging/message_service.py` + +* `class MessageService`: + * `async def store_message(sender_id: str, recipient_id: str, content: str, timestamp: datetime) -> MessageSchema`: Stores a message in the database. + * `async def get_message_history(user_id1: str, user_id2: str, limit: int = 100, offset: int = 0) -> list[MessageSchema]`: Retrieves chat history between two users. + * `async def get_group_message_history(group_id: str, limit: int = 100, offset: int = 0) -> list[MessageSchema]`: Retrieves group message history. + +## 6. Database Schema (PostgreSQL) + +* **`users` table:** + * `id`: SERIAL PRIMARY KEY + * `username`: VARCHAR(50) UNIQUE NOT NULL + * `email`: VARCHAR(100) UNIQUE NOT NULL + * `hashed_password`: VARCHAR(255) NOT NULL + * `full_name`: VARCHAR(100) + * `created_at`: TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + * `last_login_at`: TIMESTAMP WITH TIME ZONE + +* **`messages` table:** + * `id`: SERIAL PRIMARY KEY + * `sender_id`: INTEGER REFERENCES `users`(`id`) NOT NULL + * `recipient_id`: INTEGER REFERENCES `users`(`id`) NULL (for one-to-one messages) + * `group_id`: INTEGER REFERENCES `groups`(`id`) NULL (for group messages) + * `content`: TEXT NOT NULL + * `sent_at`: TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + * `is_read`: BOOLEAN DEFAULT FALSE + +* **`groups` table:** (For group chat functionality) + * `id`: SERIAL PRIMARY KEY + * `name`: VARCHAR(100) NOT NULL + * `description`: TEXT + * `created_by`: INTEGER REFERENCES `users`(`id`) NOT NULL + * `created_at`: TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + +* **`group_members` table:** (Many-to-many relationship between users and groups) + * `id`: SERIAL PRIMARY KEY + * `user_id`: INTEGER REFERENCES `users`(`id`) NOT NULL + * `group_id`: INTEGER REFERENCES `groups`(`id`) NOT NULL + * `joined_at`: TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP + * UNIQUE (`user_id`, `group_id`) + +* **Constraints/Indexes:** + * Indexes on `users.username`, `users.email`. + * Indexes on `messages.sender_id`, `messages.recipient_id`, `messages.group_id`, `messages.sent_at`. + * Indexes on `groups.name`. + * Foreign key constraints as defined above. + +## 7. Error Handling Strategy + +* **HTTP API Endpoints:** + * Use standard HTTP status codes (e.g., 200 OK, 201 Created, 400 Bad Request, 401 Unauthorized, 403 Forbidden, 404 Not Found, 500 Internal Server Error). + * FastAPI's `HTTPException` will be used for standard error responses. + * Response body for errors: `{"detail": "Error message or description"}`. +* **WebSocket Communication:** + * Define a standard message format for errors, e.g., `{"type": "error", "payload": {"code": , "message": ""}}`. + * `1xxx` series WebSocket close codes will be used where appropriate. + * Examples of error codes: + * `1001`: Authentication failed + * `1002`: Invalid message format + * `1003`: Target user offline (if not queuing messages) + * `1004`: Rate limit exceeded +* **Server-Side Errors:** + * All unexpected errors will be caught at a global level and logged. + * A generic error message will be sent to the client to avoid exposing sensitive details. + +## 8. Logging Strategy + +* **Library:** Standard Python `logging` module, configured by FastAPI/Uvicorn. +* **Log Levels:** + * `DEBUG`: Detailed information, typically of interest only when diagnosing problems. (e.g., raw incoming/outgoing messages, connection attempts). + * `INFO`: Confirmation that things are working as expected. (e.g., user login, message sent, server startup). + * `WARNING`: An indication that something unexpected happened, or indicative of some problem in the near future (e.g., 'disk space low'). (e.g., failed login attempt, message delivery retry). + * `ERROR`: Due to a more serious problem, the software has not been able to perform some function. (e.g., database connection failure, unhandled exception in a request). + * `CRITICAL`: A serious error, indicating that the program itself may be unable to continue running. +* **Log Format:** + * `%(asctime)s - %(name)s - %(levelname)s - %(module)s.%(funcName)s:%(lineno)d - %(message)s` + * Example: `2023-10-27 10:00:00,000 - uvicorn.access - INFO - main.handle_request:123 - GET /users/me HTTP/1.1 200 OK` +* **Log Output:** + * Console (stdout/stderr) during development. + * File-based logging in production (e.g., `/var/log/ccs/ccs.log`) with log rotation. +* **Key Information to Log:** + * Application startup and shutdown. + * Incoming requests (HTTP and WebSocket connections) with relevant metadata (IP, user_id if authenticated). + * Authentication successes and failures. + * Message processing details (sender, receiver/group, timestamp) - potentially at DEBUG level for content. + * Database queries (optional, can be verbose, usually enabled at DEBUG level). + * All errors and exceptions with stack traces. + * Presence updates (user connected/disconnected). + +## 9. Security Considerations (Initial Thoughts) + +* **Input Validation:** All incoming data (HTTP request bodies, WebSocket messages) will be strictly validated using Pydantic models. +* **Password Hashing:** `passlib` library with a strong hashing algorithm (e.g., bcrypt, Argon2). +* **JWT Security:** + * Use HTTPS for all communication. + * Strong, secret key for JWT signing. + * Short-lived access tokens, implement refresh token mechanism if needed. +* **WebSocket Security:** + * `wss://` (WebSocket Secure) in production. + * Authenticate WebSocket connections promptly after establishment. +* **Rate Limiting:** Consider implementing rate limiting on API endpoints and WebSocket messages to prevent abuse. +* **Dependency Management:** Keep dependencies up-to-date to patch known vulnerabilities. + +## 10. Scalability Considerations (Initial Thoughts) + +* **Statelessness:** Design services to be as stateless as possible to allow horizontal scaling. User session/connection info might need a shared store (e.g., Redis) if scaling beyond one server instance. +* **Asynchronous Operations:** Leverage Python's `asyncio` and FastAPI's async capabilities to handle many concurrent connections efficiently. +* **Database Optimization:** Proper indexing, connection pooling. Consider read replicas for the database in the future. +* **Load Balancing:** A load balancer will be needed if deploying multiple CCS instances. +* **Message Queues (Advanced):** For very high throughput or to decouple services further, a message queue (e.g., RabbitMQ, Kafka) could be introduced between message reception and processing/delivery. + +This document provides a foundational design. Further details will be elaborated during the implementation phase of each module. diff --git a/ccs/__pycache__/main.cpython-312.pyc b/ccs/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000..0c388704e8 Binary files /dev/null and b/ccs/__pycache__/main.cpython-312.pyc differ diff --git a/ccs/core/__pycache__/config.cpython-312.pyc b/ccs/core/__pycache__/config.cpython-312.pyc new file mode 100644 index 0000000000..a25f7cc788 Binary files /dev/null and b/ccs/core/__pycache__/config.cpython-312.pyc differ diff --git a/ccs/core/config.py b/ccs/core/config.py new file mode 100644 index 0000000000..2f35243aab --- /dev/null +++ b/ccs/core/config.py @@ -0,0 +1,26 @@ +from pydantic_settings import BaseSettings +from typing import Optional + +class Settings(BaseSettings): + APP_NAME: str = "Central Communication Server" + DEBUG: bool = True # Set to True for development to enable init_db() + SECRET_KEY: str = "your-secret-key" # CHANGE THIS! + ALGORITHM: str = "HS256" + ACCESS_TOKEN_EXPIRE_MINUTES: int = 30 + + # Corrected DATABASE_URL with a placeholder numeric port + DATABASE_URL: str = "postgresql://user:password@host:5432/dbname" + + # Optional Redis settings for caching/presence + REDIS_HOST: Optional[str] = None + REDIS_PORT: int = 6379 + + class Config: + env_file = ".env" # If you want to use a .env file + env_file_encoding = 'utf-8' + +settings = Settings() + +# Example usage: +# from ccs.core.config import settings +# print(settings.DATABASE_URL) diff --git a/ccs/core/security.py b/ccs/core/security.py new file mode 100644 index 0000000000..d956b0a853 --- /dev/null +++ b/ccs/core/security.py @@ -0,0 +1,23 @@ +from passlib.context import CryptContext + +# Use bcrypt for password hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +class PasswordSecurity: + @staticmethod + def verify_password(plain_password: str, hashed_password: str) -> bool: + """Verifies a plain password against a hashed password.""" + return pwd_context.verify(plain_password, hashed_password) + + @staticmethod + def get_password_hash(password: str) -> str: + """Hashes a plain password.""" + return pwd_context.hash(password) + +# Example Usage: +# from ccs.core.security import PasswordSecurity +# +# hashed_pw = PasswordSecurity.get_password_hash("mysecretpassword") +# print(f"Hashed: {hashed_pw}") +# print(f"Verification successful: {PasswordSecurity.verify_password('mysecretpassword', hashed_pw)}") +# print(f"Verification failure: {PasswordSecurity.verify_password('wrongpassword', hashed_pw)}") diff --git a/ccs/database/__pycache__/db_models.cpython-312.pyc b/ccs/database/__pycache__/db_models.cpython-312.pyc new file mode 100644 index 0000000000..4c0331ea95 Binary files /dev/null and b/ccs/database/__pycache__/db_models.cpython-312.pyc differ diff --git a/ccs/database/__pycache__/session.cpython-312.pyc b/ccs/database/__pycache__/session.cpython-312.pyc new file mode 100644 index 0000000000..e76397d9b9 Binary files /dev/null and b/ccs/database/__pycache__/session.cpython-312.pyc differ diff --git a/ccs/database/crud.py b/ccs/database/crud.py new file mode 100644 index 0000000000..aa8819f85f --- /dev/null +++ b/ccs/database/crud.py @@ -0,0 +1,102 @@ +from sqlalchemy.orm import Session +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy.future import select +from sqlalchemy import update as sqlalchemy_update, delete as sqlalchemy_delete +from typing import Any, Dict, Generic, List, Optional, Type, TypeVar, Union + +from ccs.database.db_models import Base + +ModelType = TypeVar("ModelType", bound=Base) + +class CRUDBase(Generic[ModelType]): + def __init__(self, model: Type[ModelType]): + """ + CRUD object with default methods to Create, Read, Update, Delete (CRUD). + + **Parameters** + + * `model`: A SQLAlchemy model class + """ + self.model = model + + async def get(self, db: AsyncSession, id: Any) -> Optional[ModelType]: + statement = select(self.model).where(self.model.id == id) + result = await db.execute(statement) + return result.scalar_one_or_none() + + async def get_multi( + self, db: AsyncSession, *, skip: int = 0, limit: int = 100 + ) -> List[ModelType]: + statement = select(self.model).offset(skip).limit(limit) + result = await db.execute(statement) + return result.scalars().all() + + async def create(self, db: AsyncSession, *, obj_in: Dict[str, Any]) -> ModelType: + db_obj = self.model(**obj_in) + db.add(db_obj) + await db.commit() + await db.refresh(db_obj) + return db_obj + + async def update( + self, db: AsyncSession, *, db_obj: ModelType, obj_in: Union[Dict[str, Any]] + ) -> ModelType: + if isinstance(obj_in, dict): + update_data = obj_in + else: # Pydantic model + update_data = obj_in.model_dump(exclude_unset=True) + + for field, value in update_data.items(): + setattr(db_obj, field, value) + + db.add(db_obj) + await db.commit() + await db.refresh(db_obj) + return db_obj + + async def remove(self, db: AsyncSession, *, id: int) -> Optional[ModelType]: + obj = await self.get(db, id=id) + if obj: + await db.delete(obj) + await db.commit() + return obj + +# Example of how to use it for a specific model: +# from .db_models import User +# from .schemas import UserCreate, UserUpdate # Assuming you have Pydantic schemas +# +# class CRUDUser(CRUDBase[User]): +# async def get_by_username(self, db: AsyncSession, *, username: str) -> Optional[User]: +# statement = select(self.model).where(self.model.username == username) +# result = await db.execute(statement) +# return result.scalar_one_or_none() +# +# # Add more specific methods for the User model here +# +# user_crud = CRUDUser(User) + +# This file will contain CRUD (Create, Read, Update, Delete) operations +# for your SQLAlchemy models. +# For each model, you might have a class that inherits from CRUDBase +# or implements its own specific CRUD methods. +# +# Example: +# from .db_models import User +# from .schemas import UserCreateSchema, UserUpdateSchema # You'll need Pydantic schemas +# +# async def get_user(db: AsyncSession, user_id: int): +# return await db.get(User, user_id) +# +# async def create_user(db: AsyncSession, user: UserCreateSchema): +# db_user = User(username=user.username, email=user.email, hashed_password=user.hashed_password) +# db.add(db_user) +# await db.commit() +# await db.refresh(db_user) +# return db_user +# +# ... and so on for update, delete, and other specific queries. +# +# The CRUDBase class provides a generic way to handle most common operations. +# Specific CRUD classes for each model can inherit from it and add model-specific methods. +# For example, `class CRUDUser(CRUDBase[User]): ...` +# This helps in keeping the database interaction logic organized and reusable. diff --git a/ccs/database/db_models.py b/ccs/database/db_models.py new file mode 100644 index 0000000000..a597790dde --- /dev/null +++ b/ccs/database/db_models.py @@ -0,0 +1,91 @@ +from sqlalchemy import ( + Column, Integer, String, Text, DateTime, Boolean, ForeignKey, UniqueConstraint +) +from sqlalchemy.orm import relationship, declarative_base +from sqlalchemy.sql import func +import datetime + +Base = declarative_base() + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + username = Column(String(50), unique=True, index=True, nullable=False) + email = Column(String(100), unique=True, index=True, nullable=False) + hashed_password = Column(String(255), nullable=False) + full_name = Column(String(100), nullable=True) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + last_login_at = Column(DateTime(timezone=True), nullable=True) + + sent_messages = relationship("Message", foreign_keys="[Message.sender_id]", back_populates="sender") + received_messages = relationship("Message", foreign_keys="[Message.recipient_id]", back_populates="recipient") + # groups_created = relationship("Group", back_populates="creator") + # group_memberships = relationship("GroupMember", back_populates="user") + + +class Message(Base): + __tablename__ = "messages" + + id = Column(Integer, primary_key=True, index=True) + sender_id = Column(Integer, ForeignKey("users.id"), nullable=False) + recipient_id = Column(Integer, ForeignKey("users.id"), nullable=True) # Nullable for group messages + group_id = Column(Integer, ForeignKey("groups.id"), nullable=True) # Nullable for one-to-one messages + content = Column(Text, nullable=False) + sent_at = Column(DateTime(timezone=True), server_default=func.now(), index=True) + is_read = Column(Boolean, default=False) + + sender = relationship("User", foreign_keys=[sender_id], back_populates="sent_messages") + recipient = relationship("User", foreign_keys=[recipient_id], back_populates="received_messages") + group = relationship("Group", back_populates="messages") + + __table_args__ = ( + # Ensure either recipient_id or group_id is set, but not both (or neither for system messages?) + # For now, this logic might be better handled at the application level or via more complex SQL check constraints. + # Basic check that not both are null if we enforce one or the other. + # CheckConstraint('(recipient_id IS NOT NULL AND group_id IS NULL) OR (recipient_id IS NULL AND group_id IS NOT NULL)', name='chk_message_target'), + ) + + +class Group(Base): + __tablename__ = "groups" + + id = Column(Integer, primary_key=True, index=True) + name = Column(String(100), nullable=False, index=True) + description = Column(Text, nullable=True) + created_by_id = Column(Integer, ForeignKey("users.id"), nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + creator = relationship("User") #, back_populates="groups_created") # Add back_populates to User if this relation is kept bi-directional + messages = relationship("Message", back_populates="group") + members = relationship("GroupMember", back_populates="group") + + +class GroupMember(Base): + __tablename__ = "group_members" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id"), nullable=False) + group_id = Column(Integer, ForeignKey("groups.id"), nullable=False) + joined_at = Column(DateTime(timezone=True), server_default=func.now()) + + user = relationship("User") #, back_populates="group_memberships") # Add back_populates to User if this relation is kept bi-directional + group = relationship("Group", back_populates="members") + + __table_args__ = (UniqueConstraint('user_id', 'group_id', name='uq_user_group'),) + + +# Example of how to create tables (typically done in a main script or Alembic migration) +# from sqlalchemy import create_engine +# from ccs.core.config import settings +# +# if __name__ == "__main__": +# engine = create_engine(settings.DATABASE_URL) +# Base.metadata.create_all(bind=engine) # This creates tables if they don't exist +# print("Database tables defined (if not existing, they would be created by uncommenting create_all).") + +# Note: Relationships in User model for groups and group_memberships are commented out +# to avoid circular dependency errors until all models are fully defined and potentially +# to simplify. They can be added back if direct back-population from User is desired. +# For now, Group.creator, Group.members, GroupMember.user, and GroupMember.group provide +# the necessary relationships. Message relationships are set up. diff --git a/ccs/database/session.py b/ccs/database/session.py new file mode 100644 index 0000000000..0b98ef0dfe --- /dev/null +++ b/ccs/database/session.py @@ -0,0 +1,77 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker +from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession # For async operations if needed later + +from ccs.core.config import settings +from ccs.database.db_models import Base # To create tables + +# Synchronous engine (can be used for Alembic migrations or simple scripts) +# Added connect_args for a shorter connection timeout +sync_engine = create_engine( + settings.DATABASE_URL, + pool_pre_ping=True, + connect_args={'connect_timeout': 5} # Timeout in seconds +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=sync_engine) + +# Asynchronous engine (for FastAPI async endpoints) +# Ensure your DATABASE_URL is compatible with async drivers e.g. postgresql+asyncpg:// +async_engine = create_async_engine( + settings.DATABASE_URL.replace("postgresql://", "postgresql+asyncpg://"), + echo=settings.DEBUG, + connect_args={'timeout': 5} # For asyncpg, timeout is a top-level param in connect_args for create_async_engine +) +AsyncSessionLocal = sessionmaker( + autocommit=False, + autoflush=False, + bind=async_engine, + class_=AsyncSession, + expire_on_commit=False, # Good default for FastAPI +) + +def init_db(): + """ + Initializes the database by creating all tables defined in db_models. + This is suitable for development/testing. For production, use Alembic migrations. + """ + print("Attempting to initialize the database and create tables...") + try: + Base.metadata.create_all(bind=sync_engine) + print("Tables created successfully (if they didn't exist).") + except Exception as e: + print(f"Error creating tables: {e}") + print("Please ensure the database server is running and the DATABASE_URL is correct.") + print(f"DATABASE_URL used: {settings.DATABASE_URL}") + +async def get_db_session() -> AsyncSession: + """ + Dependency to get an async database session. + Ensures the session is closed after the request. + """ + if AsyncSessionLocal is None: # Should not happen if async_engine is initialized + raise RuntimeError("AsyncSessionLocal is not initialized.") + async with AsyncSessionLocal() as session: + try: + yield session + await session.commit() + except Exception: + await session.rollback() + raise + finally: + await session.close() + +# If you need a synchronous session for some specific background tasks or scripts: +def get_sync_db_session(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +# For manual testing of table creation: +# if __name__ == "__main__": +# print(f"Database URL: {settings.DATABASE_URL}") +# print("Attempting to create database tables directly using session.py...") +# # Make sure your database server is running and the database specified in DATABASE_URL exists. +# init_db() +# print("Table creation process finished. Check server logs for details.") diff --git a/ccs/main.py b/ccs/main.py new file mode 100644 index 0000000000..facfe44b61 --- /dev/null +++ b/ccs/main.py @@ -0,0 +1,62 @@ +from fastapi import FastAPI +from ccs.core.config import settings + +app = FastAPI( + title=settings.APP_NAME, + debug=settings.DEBUG, + # version="0.1.0", # Optional: if you want to version your API +) + +@app.on_event("startup") +async def startup_event(): + """ + Actions to perform on application startup. + For example, initializing database connections, loading ML models, etc. + """ + print("Application startup...") + # Initialize database (create tables if they don't exist) + # In a production environment, you'd typically use Alembic for migrations. + # from ccs.database.session import init_db + # if settings.DEBUG: # Optionally restrict table creation to debug mode + # print("DEBUG mode: Initializing database...") + # init_db() # <--- TEMPORARILY COMMENTED OUT FOR TESTING + # else: + # print("PRODUCTION mode: Skipping automatic database initialization.") + print("Skipping database initialization for testing.") # Added test message + + print(f"Running in {'DEBUG' if settings.DEBUG else 'PRODUCTION'} mode.") + + +@app.on_event("shutdown") +async def shutdown_event(): + """ + Actions to perform on application shutdown. + For example, closing database connections. + """ + print("Application shutdown...") + # Here you might close database connections + # from ccs.database.session import engine + # await engine.dispose() # If using an async engine that supports dispose + + +@app.get("/", tags=["Health Check"]) +async def root(): + """ + Root endpoint for health check. + """ + return {"message": f"Welcome to {settings.APP_NAME}"} + +# Placeholder for future routers +# from ccs.auth import auth_routes +# from ccs.users import user_routes # If you create separate user HTTP routes +# from ccs.messaging import websocket_routes +# +# app.include_router(auth_routes.router, prefix="/auth", tags=["Authentication"]) +# app.include_router(websocket_routes.router) # WebSocket router might not have a prefix + +if __name__ == "__main__": + import uvicorn + # This is for development purposes only. + # In production, you would run Uvicorn directly or via a process manager. + # Example: uvicorn ccs.main:app --host 0.0.0.0 --port 8000 --reload + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/minimal_app.py b/minimal_app.py new file mode 100644 index 0000000000..7952d6365d --- /dev/null +++ b/minimal_app.py @@ -0,0 +1,11 @@ +from fastapi import FastAPI + +app = FastAPI() + +@app.get("/") +async def read_root(): + return {"Hello": "World"} + +if __name__ == "__main__": + import uvicorn + uvicorn.run(app, host="0.0.0.0", port=8000) diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000..6d3465badb --- /dev/null +++ b/requirements.txt @@ -0,0 +1,16 @@ +fastapi +uvicorn[standard] +pydantic-settings +passlib[bcrypt] +sqlalchemy +psycopg2-binary +asyncpg +python-jose[cryptography] +alembic + +# For development, consider adding: +# flake8 +# black +# isort +# pytest +# pytest-asyncio