commit 2fedc1ac18f67301ab8f329700687725ca0c4799 Author: alex Date: Thu Feb 26 19:18:58 2026 +0500 init app diff --git a/__pycache__/crud.cpython-313.pyc b/__pycache__/crud.cpython-313.pyc new file mode 100644 index 0000000..671e3ff Binary files /dev/null and b/__pycache__/crud.cpython-313.pyc differ diff --git a/__pycache__/database.cpython-313.pyc b/__pycache__/database.cpython-313.pyc new file mode 100644 index 0000000..11fc52d Binary files /dev/null and b/__pycache__/database.cpython-313.pyc differ diff --git a/__pycache__/main.cpython-313.pyc b/__pycache__/main.cpython-313.pyc new file mode 100644 index 0000000..5bb7226 Binary files /dev/null and b/__pycache__/main.cpython-313.pyc differ diff --git a/__pycache__/models.cpython-313.pyc b/__pycache__/models.cpython-313.pyc new file mode 100644 index 0000000..be3eaaa Binary files /dev/null and b/__pycache__/models.cpython-313.pyc differ diff --git a/__pycache__/schemas.cpython-313.pyc b/__pycache__/schemas.cpython-313.pyc new file mode 100644 index 0000000..63c4f16 Binary files /dev/null and b/__pycache__/schemas.cpython-313.pyc differ diff --git a/crud.py b/crud.py new file mode 100644 index 0000000..b828eda --- /dev/null +++ b/crud.py @@ -0,0 +1,84 @@ +from sqlalchemy.orm import Session +import models +import schemas + +# Tasks +def get_task(db: Session, task_id: int, telegram_user_id: str = None): + query = db.query(models.Task).filter(models.Task.id == task_id) + if telegram_user_id: + query = query.filter(models.Task.telegram_user_id == telegram_user_id) + return query.first() + +def get_tasks(db: Session, skip: int = 0, limit: int = 100, telegram_user_id: str = None, status: models.TaskStatus = None): + query = db.query(models.Task) + if telegram_user_id: + query = query.filter(models.Task.telegram_user_id == telegram_user_id) + if status: + query = query.filter(models.Task.status == status) + return query.offset(skip).limit(limit).all() + +def create_task(db: Session, task: schemas.TaskCreate): + db_task = models.Task(**task.model_dump()) + db.add(db_task) + db.commit() + db.refresh(db_task) + return db_task + +def update_task(db: Session, task_id: int, task_update: schemas.TaskUpdate, telegram_user_id: str = None): + db_task = get_task(db, task_id, telegram_user_id) + if not db_task: + return None + + update_data = task_update.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_task, key, value) + + db.commit() + db.refresh(db_task) + return db_task + +def delete_task(db: Session, task_id: int, telegram_user_id: str = None): + db_task = get_task(db, task_id, telegram_user_id) + if db_task: + db.delete(db_task) + db.commit() + return True + return False + +# Reminders +def get_reminder(db: Session, reminder_id: int): + return db.query(models.Reminder).filter(models.Reminder.id == reminder_id).first() + +def get_reminders(db: Session, task_id: int = None, skip: int = 0, limit: int = 100): + query = db.query(models.Reminder) + if task_id: + query = query.filter(models.Reminder.task_id == task_id) + return query.offset(skip).limit(limit).all() + +def create_reminder(db: Session, reminder: schemas.ReminderCreate): + db_reminder = models.Reminder(**reminder.model_dump()) + db.add(db_reminder) + db.commit() + db.refresh(db_reminder) + return db_reminder + +def update_reminder(db: Session, reminder_id: int, reminder_update: schemas.ReminderUpdate): + db_reminder = get_reminder(db, reminder_id) + if not db_reminder: + return None + + update_data = reminder_update.model_dump(exclude_unset=True) + for key, value in update_data.items(): + setattr(db_reminder, key, value) + + db.commit() + db.refresh(db_reminder) + return db_reminder + +def delete_reminder(db: Session, reminder_id: int): + db_reminder = get_reminder(db, reminder_id) + if db_reminder: + db.delete(db_reminder) + db.commit() + return True + return False diff --git a/database.py b/database.py new file mode 100644 index 0000000..aa18af6 --- /dev/null +++ b/database.py @@ -0,0 +1,19 @@ +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker, declarative_base + +SQLALCHEMY_DATABASE_URL = "sqlite:///./tasks.db" +# connect_args={"check_same_thread": False} is required for SQLite in FastAPI +engine = create_engine( + SQLALCHEMY_DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +# Dependency +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() diff --git a/int.txt b/int.txt new file mode 100644 index 0000000..601024e --- /dev/null +++ b/int.txt @@ -0,0 +1,35 @@ +Task and Reminder Manager Backend - Walkthrough +What Was Accomplished +I successfully developed the backend infrastructure for the Task and Reminder Manager application, exposing a REST API built with FastAPI that meets the user requirements. I set up the database, models, and endpoints necessary to manage tasks and reminders. + +Key Changes Made +Application Structure: Created a modular FastAPI backend ( +main.py +, +database.py +, +models.py +, +schemas.py +, +crud.py +). +Database System: Implemented SQLite functionality via SQLAlchemy, with specific models adapted to track user execution and messaging contexts. +Designed +Task + model containing the telegram_user_id and the status (pending, completed). +Designed +Reminder + model tightly coupled to +Task +, capable of deep tracking: trigger_time, recurrence_type (daily, custom, etc.), and notification toggles (notify_push, notify_telegram). +Data Validation: Built Pydantic schemas representing creation payloads, updates, and serialized API responses to ensure safe data boundaries. +API Endpoints: Built isolated routers to handle operations across tasks ( +routers/tasks.py +) and reminders ( +routers/reminders.py +). Examples include retrieving tasks specific to a telegram_user_id, handling task completion logic (via PUT endpoint updates), and constructing sophisticated reminders. +Validation Results +Verified that all necessary dependencies (including updated Python 3.13-compatible versions) install inside a valid virtual environment. +Verified successful Uvicorn server boot without errors. +Fired test API requests to generate entries into SQLite, returning 200 success responses. The system's CRUD functionality handles database commits successfully and retrieves nested relationships gracefully. \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..16b4335 --- /dev/null +++ b/main.py @@ -0,0 +1,34 @@ +from fastapi import FastAPI +from fastapi.middleware.cors import CORSMiddleware +import models +from database import engine +from routers import tasks, reminders + +# Create database tables +models.Base.metadata.create_all(bind=engine) + +app = FastAPI( + title="Task Manager API", + description="Backend API for managing tasks and reminders for Web and Telegram.", + version="1.0.0" +) + +# CORS configuration +origins = [ + "*" # Adjust this in production +] +app.add_middleware( + CORSMiddleware, + allow_origins=origins, + allow_credentials=True, + allow_methods=["*"], + allow_headers=["*"], +) + +# Include routers +app.include_router(tasks.router) +app.include_router(reminders.router) + +@app.get("/") +def read_root(): + return {"message": "Welcome to the Task Manager API"} diff --git a/models.py b/models.py new file mode 100644 index 0000000..62042c2 --- /dev/null +++ b/models.py @@ -0,0 +1,46 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime, ForeignKey, Enum +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +import enum +from database import Base + +class TaskStatus(str, enum.Enum): + pending = "pending" + completed = "completed" + +class RecurrenceType(str, enum.Enum): + none = "none" + daily = "daily" + weekly = "weekly" + monthly = "monthly" + custom = "custom" + +class Task(Base): + __tablename__ = "tasks" + + id = Column(Integer, primary_key=True, index=True) + title = Column(String, index=True, nullable=False) + description = Column(String, nullable=True) + telegram_user_id = Column(String, index=True, nullable=False) + status = Column(Enum(TaskStatus), default=TaskStatus.pending, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + reminders = relationship("Reminder", back_populates="task", cascade="all, delete-orphan") + +class Reminder(Base): + __tablename__ = "reminders" + + id = Column(Integer, primary_key=True, index=True) + task_id = Column(Integer, ForeignKey("tasks.id"), nullable=False) + trigger_time = Column(DateTime(timezone=True), nullable=False) + recurrence_type = Column(Enum(RecurrenceType), default=RecurrenceType.none, nullable=False) + recurrence_interval = Column(Integer, nullable=True) # E.g. every X minutes/hours if custom + remind_count_limit = Column(Integer, default=0, nullable=False) # 0 for infinite + remind_count_current = Column(Integer, default=0, nullable=False) + notify_push = Column(Boolean, default=False, nullable=False) + notify_telegram = Column(Boolean, default=False, nullable=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now()) + + task = relationship("Task", back_populates="reminders") diff --git a/pip_install.log b/pip_install.log new file mode 100644 index 0000000..e923b38 --- /dev/null +++ b/pip_install.log @@ -0,0 +1,60 @@ +Collecting fastapi==0.110.0 (from -r requirements.txt (line 1)) + Using cached fastapi-0.110.0-py3-none-any.whl.metadata (25 kB) +Collecting uvicorn==0.27.1 (from -r requirements.txt (line 2)) + Using cached uvicorn-0.27.1-py3-none-any.whl.metadata (6.3 kB) +Collecting sqlalchemy==2.0.28 (from -r requirements.txt (line 3)) + Using cached SQLAlchemy-2.0.28-py3-none-any.whl.metadata (9.6 kB) +Collecting pydantic==2.6.4 (from -r requirements.txt (line 4)) + Using cached pydantic-2.6.4-py3-none-any.whl.metadata (85 kB) +Collecting pydantic-settings==2.2.1 (from -r requirements.txt (line 5)) + Using cached pydantic_settings-2.2.1-py3-none-any.whl.metadata (3.1 kB) +Collecting starlette<0.37.0,>=0.36.3 (from fastapi==0.110.0->-r requirements.txt (line 1)) + Using cached starlette-0.36.3-py3-none-any.whl.metadata (5.9 kB) +Collecting typing-extensions>=4.8.0 (from fastapi==0.110.0->-r requirements.txt (line 1)) + Using cached typing_extensions-4.15.0-py3-none-any.whl.metadata (3.3 kB) +Collecting annotated-types>=0.4.0 (from pydantic==2.6.4->-r requirements.txt (line 4)) + Using cached annotated_types-0.7.0-py3-none-any.whl.metadata (15 kB) +Collecting pydantic-core==2.16.3 (from pydantic==2.6.4->-r requirements.txt (line 4)) + Using cached pydantic_core-2.16.3.tar.gz (368 kB) + Installing build dependencies: started + Installing build dependencies: finished with status 'done' + Getting requirements to build wheel: started + Getting requirements to build wheel: finished with status 'done' + Installing backend dependencies: started + Installing backend dependencies: finished with status 'done' + Preparing metadata (pyproject.toml): started + Preparing metadata (pyproject.toml): finished with status 'error' + error: subprocess-exited-with-error + + Preparing metadata (pyproject.toml) did not run successfully. + exit code: 1 + + [14 lines of output] + Python reports SOABI: cp313-win_amd64 + Computed rustc target triple: x86_64-pc-windows-msvc + Installation directory: C:\Users\patro\AppData\Local\puccinialin\puccinialin\Cache + Rustup already downloaded + Installing rust to C:\Users\patro\AppData\Local\puccinialin\puccinialin\Cache\rustup + warn: installing msvc toolchain without its prerequisites + error: could not read metadata for file: 'C:\Users\patro\AppData\Local\puccinialin\puccinialin\Cache\rustup-init\rustup-init.exe': ╨б╨╕╤Б╤В╨╡╨╝╨╡ ╨╜╨╡ ╤Г╨┤╨░╨╡╤В╤Б╤П ╨╜╨░╨╣╤В╨╕ ╤Г╨║╨░╨╖╨░╨╜╨╜╤Л╨╣ ╨┐╤Г╤В╤М. (os error 3) + + Cargo, the Rust package manager, is not installed or is not on PATH. + This package requires Rust and Cargo to compile extensions. Install it through + the system's package manager or via https://rustup.rs/ + + Checking for Rust toolchain.... + Rust not found, installing into a temporary directory + [end of output] + + note: This error originates from a subprocess, and is likely not a problem with pip. + +[notice] A new release of pip is available: 25.3 -> 26.0.1 +[notice] To update, run: G:\CodeAssistent\task\api\task-api\.venv\Scripts\python.exe -m pip install --upgrade pip +error: metadata-generation-failed + +Encountered error while generating package metadata. + +pydantic-core + +note: This is an issue with the package mentioned above, not pip. +hint: See above for details. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..9676ea2 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,5 @@ +fastapi +uvicorn +sqlalchemy +pydantic +pydantic-settings diff --git a/routers/__pycache__/reminders.cpython-313.pyc b/routers/__pycache__/reminders.cpython-313.pyc new file mode 100644 index 0000000..cf35d7f Binary files /dev/null and b/routers/__pycache__/reminders.cpython-313.pyc differ diff --git a/routers/__pycache__/tasks.cpython-313.pyc b/routers/__pycache__/tasks.cpython-313.pyc new file mode 100644 index 0000000..7b8dc92 Binary files /dev/null and b/routers/__pycache__/tasks.cpython-313.pyc differ diff --git a/routers/reminders.py b/routers/reminders.py new file mode 100644 index 0000000..3736621 --- /dev/null +++ b/routers/reminders.py @@ -0,0 +1,53 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List + +import crud +import models +import schemas +from database import get_db + +router = APIRouter( + prefix="/reminders", + tags=["reminders"] +) + +@router.post("/", response_model=schemas.Reminder) +def create_reminder(reminder: schemas.ReminderCreate, db: Session = Depends(get_db)): + # Verify task exists + db_task = crud.get_task(db, task_id=reminder.task_id) + if not db_task: + raise HTTPException(status_code=404, detail="Task not found") + + return crud.create_reminder(db=db, reminder=reminder) + +@router.get("/", response_model=List[schemas.Reminder]) +def read_reminders( + skip: int = 0, + limit: int = 100, + task_id: int = Query(None, description="Filter by Task ID"), + db: Session = Depends(get_db) +): + reminders = crud.get_reminders(db, skip=skip, limit=limit, task_id=task_id) + return reminders + +@router.get("/{reminder_id}", response_model=schemas.Reminder) +def read_reminder(reminder_id: int, db: Session = Depends(get_db)): + db_reminder = crud.get_reminder(db, reminder_id=reminder_id) + if db_reminder is None: + raise HTTPException(status_code=404, detail="Reminder not found") + return db_reminder + +@router.put("/{reminder_id}", response_model=schemas.Reminder) +def update_reminder_endpoint(reminder_id: int, reminder: schemas.ReminderUpdate, db: Session = Depends(get_db)): + db_reminder = crud.update_reminder(db, reminder_id=reminder_id, reminder_update=reminder) + if db_reminder is None: + raise HTTPException(status_code=404, detail="Reminder not found") + return db_reminder + +@router.delete("/{reminder_id}") +def delete_reminder_endpoint(reminder_id: int, db: Session = Depends(get_db)): + success = crud.delete_reminder(db, reminder_id=reminder_id) + if not success: + raise HTTPException(status_code=404, detail="Reminder not found") + return {"message": "Reminder successfully deleted"} diff --git a/routers/tasks.py b/routers/tasks.py new file mode 100644 index 0000000..d1996dd --- /dev/null +++ b/routers/tasks.py @@ -0,0 +1,49 @@ +from fastapi import APIRouter, Depends, HTTPException, Query +from sqlalchemy.orm import Session +from typing import List + +import crud +import models +import schemas +from database import get_db + +router = APIRouter( + prefix="/tasks", + tags=["tasks"] +) + +@router.post("/", response_model=schemas.Task) +def create_task(task: schemas.TaskCreate, db: Session = Depends(get_db)): + return crud.create_task(db=db, task=task) + +@router.get("/", response_model=List[schemas.Task]) +def read_tasks( + skip: int = 0, + limit: int = 100, + telegram_user_id: str = Query(None, description="Filter by Telegram User ID"), + status: models.TaskStatus = Query(None, description="Filter by task status"), + db: Session = Depends(get_db) +): + tasks = crud.get_tasks(db, skip=skip, limit=limit, telegram_user_id=telegram_user_id, status=status) + return tasks + +@router.get("/{task_id}", response_model=schemas.Task) +def read_task(task_id: int, telegram_user_id: str = Query(None, description="Telegram User ID for authorization"), db: Session = Depends(get_db)): + db_task = crud.get_task(db, task_id=task_id, telegram_user_id=telegram_user_id) + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found") + return db_task + +@router.put("/{task_id}", response_model=schemas.Task) +def update_task_endpoint(task_id: int, task: schemas.TaskUpdate, telegram_user_id: str = Query(None, description="Telegram User ID for authorization"), db: Session = Depends(get_db)): + db_task = crud.update_task(db, task_id=task_id, task_update=task, telegram_user_id=telegram_user_id) + if db_task is None: + raise HTTPException(status_code=404, detail="Task not found or you don't have access") + return db_task + +@router.delete("/{task_id}") +def delete_task_endpoint(task_id: int, telegram_user_id: str = Query(None, description="Telegram User ID for authorization"), db: Session = Depends(get_db)): + success = crud.delete_task(db, task_id=task_id, telegram_user_id=telegram_user_id) + if not success: + raise HTTPException(status_code=404, detail="Task not found or you don't have access") + return {"message": "Task successfully deleted"} diff --git a/schemas.py b/schemas.py new file mode 100644 index 0000000..aa9e114 --- /dev/null +++ b/schemas.py @@ -0,0 +1,60 @@ +from pydantic import BaseModel, Field +from datetime import datetime +from typing import Optional, List +from models import TaskStatus, RecurrenceType + +# Reminder Schemas +class ReminderBase(BaseModel): + trigger_time: datetime + recurrence_type: RecurrenceType = RecurrenceType.none + recurrence_interval: Optional[int] = None + remind_count_limit: int = 0 + notify_push: bool = False + notify_telegram: bool = False + +class ReminderCreate(ReminderBase): + task_id: int + +class ReminderUpdate(BaseModel): + trigger_time: Optional[datetime] = None + recurrence_type: Optional[RecurrenceType] = None + recurrence_interval: Optional[int] = None + remind_count_limit: Optional[int] = None + remind_count_current: Optional[int] = None + notify_push: Optional[bool] = None + notify_telegram: Optional[bool] = None + +class Reminder(ReminderBase): + id: int + task_id: int + remind_count_current: int + created_at: datetime + updated_at: datetime + + class Config: + from_attributes = True + + +# Task Schemas +class TaskBase(BaseModel): + title: str + description: Optional[str] = None + telegram_user_id: str + +class TaskCreate(TaskBase): + pass + +class TaskUpdate(BaseModel): + title: Optional[str] = None + description: Optional[str] = None + status: Optional[TaskStatus] = None + +class Task(TaskBase): + id: int + status: TaskStatus + created_at: datetime + updated_at: datetime + reminders: List[Reminder] = [] + + class Config: + from_attributes = True diff --git a/tasks.db b/tasks.db new file mode 100644 index 0000000..5dabf13 Binary files /dev/null and b/tasks.db differ