aki-008 commited on
Commit
a8e2a77
·
1 Parent(s): 6930a48
.gitignore CHANGED
@@ -219,4 +219,4 @@ playground/psql_driver.ipynb
219
  Backend/Time complexity cheatsheet.pdf
220
  test/1000-data-science-questions-answers.json
221
 
222
- test/test.ipynb
 
219
  Backend/Time complexity cheatsheet.pdf
220
  test/1000-data-science-questions-answers.json
221
 
222
+ test/test.ipynb
Backend/app/api/deps.py CHANGED
@@ -6,6 +6,8 @@ from jose import JWTError, jwt
6
  from app.database import async_session_maker
7
  from app.models import User
8
  from app.config import settings
 
 
9
 
10
  security = HTTPBearer()
11
 
@@ -48,3 +50,10 @@ async def get_current_user(
48
 
49
  return user
50
 
 
 
 
 
 
 
 
 
6
  from app.database import async_session_maker
7
  from app.models import User
8
  from app.config import settings
9
+ from fastapi import Request
10
+ from chromadb import AsyncHttpClient
11
 
12
  security = HTTPBearer()
13
 
 
50
 
51
  return user
52
 
53
+
54
+
55
+ async def get_chroma_client(request: Request) -> AsyncHttpClient:
56
+ client = getattr(request.app.state, "chroma_client", None)
57
+ if client is None:
58
+ raise RuntimeError("ChromaDB client is not initialized in App State.")
59
+ return client
Backend/app/api/v1/endpoints/prompts.py ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+
3
+ SYSTEM_PROMPT = """
4
+ You are an AI question-generation agent.
5
+ Your task is to generate a batch of 10 high-quality MCQ questions strictly based on the following inputs:
6
+
7
+ - {parsed_info}
8
+ - {user_prompt}
9
+ - {mcq_style}
10
+ - {retrieved_docs}
11
+
12
+ -----------------------
13
+ GENERATION RULES
14
+ -----------------------
15
+ 1. Generate exactly 10 MCQs.
16
+ 2. Use only information from the provided inputs.
17
+ 3. Each question must be unambiguous, factual, and supported by the given data.
18
+ 4. Each MCQ MUST have exactly four options.
19
+ 5. Only one correct answer is allowed.
20
+ 6. Explanations must be short and directly justify the answer.
21
+ 7. `User_response` must ALWAYS remain an empty string.
22
+ 8. Output MUST be a valid JSON array containing 10 objects.
23
+ 9. Output MUST contain nothing except the JSON array (no commentary or markdown).
24
+
25
+ -----------------------
26
+ REQUIRED JSON FORMAT FOR EACH QUESTION
27
+ -----------------------
28
+ {
29
+ "question": "Which of the following CLI command can also be used to rename files?",
30
+ "options": [
31
+ "rm",
32
+ "mv",
33
+ "rm -r",
34
+ "none of the mentioned"
35
+ ],
36
+ "answer": "b",
37
+ "explanation": "Explanation: mv stands for move.",
38
+ "User_response": ""
39
+ }
40
+
41
+ -----------------------
42
+ ANSWER KEY RULES
43
+ -----------------------
44
+ - 'a' -> options[0]
45
+ - 'b' -> options[1]
46
+ - 'c' -> options[2]
47
+ - 'd' -> options[3]
48
+
49
+ Strictly follow the JSON structure and generate exactly 10 MCQs.
50
+ """
Backend/app/api/v1/endpoints/quiz.py CHANGED
@@ -1,24 +1,37 @@
1
- from fastapi import APIRouter, HTTPException, Depends, status
2
- from sqlalchemy.ext.asyncio import AsyncSession
3
- from sqlalchemy import select
4
- from typing import List
5
- from app.schema import StudentCreate, StudentUpdate, StudentResponse
6
- from app.models import Student, User
7
- from app.api.deps import get_db, get_current_user
8
  from app.schema import Quiz_input
 
9
 
10
  router = APIRouter()
11
 
 
 
 
 
 
12
  # @router.post("/", response_model=StudentResponse, status_code=status.HTTP_201_CREATED)
13
  # async def generate_quiz(
14
  # Input_model: Quiz_input, db: AsyncSession = Depends(get_db),
15
  # current_user: User = Depends(get_current_user)):
16
 
17
- # try:
18
- # if Input_model.parsed_doc and Input_model.user_prompt and Input_model.choice:
 
 
19
 
20
 
 
21
 
22
- #--------Helper Functions--------#
23
 
24
- def prompt_builder()
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from chromadb import AsyncHttpClient
3
+ from app.models import User
4
+ from app.api.deps import get_db, get_current_user, get_chroma_client
 
 
 
5
  from app.schema import Quiz_input
6
+ from prompts import SYSTEM_PROMPT
7
 
8
  router = APIRouter()
9
 
10
+
11
+
12
+
13
+
14
+
15
  # @router.post("/", response_model=StudentResponse, status_code=status.HTTP_201_CREATED)
16
  # async def generate_quiz(
17
  # Input_model: Quiz_input, db: AsyncSession = Depends(get_db),
18
  # current_user: User = Depends(get_current_user)):
19
 
20
+
21
+ # if Input_model.parsed_doc and Input_model.user_prompt and Input_model.choice:
22
+ # prompt = prompt_builder(Input_model.parsed_doc, Input_model.user_prompt, Input_model.choice)
23
+
24
 
25
 
26
+ # #--------Helper Functions--------#
27
 
28
+ # def get_embed()
29
 
30
+ # def prompt_builder(parsed_doc:str, user_prompt:str, choice:str):
31
+ # retrieved_docs = get_embed()
32
+ # prompt = SYSTEM_PROMPT.format(
33
+ # parsed_info=parsed_doc,
34
+ # user_prompt=user_prompt,
35
+ # mcq_style=choice,
36
+ # # retrieved_docs=
37
+ # )
Backend/app/config.py CHANGED
@@ -12,6 +12,10 @@ class Settings(BaseSettings):
12
 
13
  CORS_ORIGINS: list = ["*"]
14
 
 
 
 
 
15
  class Config:
16
  env_file = ".env"
17
  extra = "ignore" # quiz
 
12
 
13
  CORS_ORIGINS: list = ["*"]
14
 
15
+ chroma_host: str
16
+ chroma_port: int
17
+ chroma_collection: str
18
+
19
  class Config:
20
  env_file = ".env"
21
  extra = "ignore" # quiz
Backend/app/main.py CHANGED
@@ -5,6 +5,8 @@ from datetime import datetime
5
  from app.config import settings
6
  from app.database import engine, Base
7
  from app.api.v1.api import api_router
 
 
8
 
9
 
10
  @asynccontextmanager
@@ -16,6 +18,11 @@ async def lifespan(app: FastAPI):
16
  async with engine.begin() as conn:
17
  await conn.run_sync(Base.metadata.create_all)
18
 
 
 
 
 
 
19
  print("✅ Tables ready!")
20
  yield
21
  print("🧹 Server shutting down:", datetime.now())
 
5
  from app.config import settings
6
  from app.database import engine, Base
7
  from app.api.v1.api import api_router
8
+ import chromadb
9
+
10
 
11
 
12
  @asynccontextmanager
 
18
  async with engine.begin() as conn:
19
  await conn.run_sync(Base.metadata.create_all)
20
 
21
+ app.state.chroma_client = await chromadb.AsyncHttpClient(
22
+ host=settings.chroma_host,
23
+ port=settings.chroma_port
24
+ )
25
+
26
  print("✅ Tables ready!")
27
  yield
28
  print("🧹 Server shutting down:", datetime.now())
Backend/app/services/inital_data.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from chromadb import AsyncHttpClient
2
+ from chromadb.utils import embedding_functions
3
+
4
+ async def ingest_start(client: AsyncHttpClient, collection_name:str):
5
+
6
+ try:
7
+ await client.get_collection(name=collection_name)
8
+ print(f"Collection '{collection_name}' already exists. Skipping initial ingestion.")
9
+ return
10
+ except Exception:
11
+ print(f"Collection '{collection_name}' not found. Starting initial ingestion...")
12
+ pass
13
+
14
+
Backend/app/vector_db.py ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ import chromadb
2
+ from chromadb.config import Settings
3
+
4
+ chroma_client = None
test/db CRUD.py DELETED
@@ -1,88 +0,0 @@
1
- import asyncio
2
- from datetime import datetime
3
- from sqlalchemy import String, Integer, DateTime
4
- from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
5
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
6
- from sqlalchemy import select
7
-
8
- DATABASE_URL = "postgresql+asyncpg://postgres:690869@localhost:5432/studentdb"
9
-
10
- engine = create_async_engine(DATABASE_URL, echo=True)
11
- async_session = async_sessionmaker(engine, expire_on_commit=False)
12
-
13
- class Base(DeclarativeBase):
14
- pass
15
-
16
- class Student(Base):
17
- __tablename__ = "students"
18
-
19
- id: Mapped[int] = mapped_column(primary_key=True)
20
- name: Mapped[str] = mapped_column(String(100))
21
- email: Mapped[str] = mapped_column(String(100))
22
- age: Mapped[int]
23
- grade: Mapped[str] = mapped_column(String(5))
24
- created_at : Mapped[datetime] = mapped_column(default=datetime.utcnow)
25
-
26
- async def create_table():
27
- async with engine.begin() as conn:
28
- await conn.run_sync(Base.metadata.create_all)
29
- print('Tables created successfully !')
30
-
31
- async def add_student(name:str, email: str, age: int, grade: str):
32
- async with async_session() as session:
33
- async with session.begin():
34
- student = Student( name = name, email= email, age=age, grade=grade)
35
- session.add(student)
36
- print(f"added student{name}")
37
-
38
- async def list_students():
39
- async with async_session() as session:
40
- result = await session.execute(select(Student))
41
- students = result.scalars().all()
42
-
43
- for s in students:
44
- print(f" - {s.id}: {s.name}, {s.email}, Grade: {s.grade}, {s.age}")
45
-
46
- async def update_student(student_id: int, new_name: str = None, new_age : int = None, new_grade : int = None ):
47
- async with async_session() as session:
48
- async with session.begin():
49
- student = await session.get(Student, student_id)
50
- if not student:
51
- print(f"❌ Student with id {student_id} not found.")
52
- return
53
- if new_name:
54
- student.name = new_name
55
- if new_grade:
56
- student.grade = new_grade
57
- if new_age:
58
- student.age = new_age
59
-
60
- # await session.commit() ## no need
61
- print(f"✏️ Updated student ID {student_id}")
62
-
63
- async def delete_student(student_id: int):
64
- async with async_session() as session:
65
- async with session.begin():
66
- student = await session.get(Student, student_id)
67
- if not student:
68
- print(f"❌ Student with id {student_id} not found.")
69
- return
70
-
71
- await session.delete(student)
72
- # await session.commit() ## no need
73
- print(f"🗑️ Deleted student ID {student_id}")
74
-
75
-
76
- async def main():
77
- await create_table()
78
- await add_student("Akshat Mehta", "akshat@example.com", 21, "A+")
79
- await add_student("Nikhil Sharma", "nikhil@example.com", 22, "B")
80
- await list_students()
81
-
82
- await update_student(1, new_age=52)
83
- await delete_student(2)
84
- print('-'*100)
85
- await list_students()
86
-
87
- if __name__ == "__main__":
88
- asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/db_fastapi_CRUD.py DELETED
@@ -1,127 +0,0 @@
1
- import asyncio
2
- from typing import List, Optional
3
- from contextlib import asynccontextmanager
4
- from datetime import datetime
5
- from fastapi import FastAPI, HTTPException, status
6
- from sqlalchemy import String, Integer, DateTime, select
7
- from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession
8
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
9
- from pydantic import BaseModel, ConfigDict
10
-
11
-
12
- DATABASE_URL = "postgresql+asyncpg://postgres:690869@localhost:5432/studentdb"
13
-
14
- engine = create_async_engine(DATABASE_URL, echo=True)
15
- async_session = async_sessionmaker(engine, expire_on_commit=False)
16
-
17
- class Base(DeclarativeBase):
18
- pass
19
-
20
- class Student(Base):
21
- __tablename__ = "students"
22
-
23
- id: Mapped[int] = mapped_column(primary_key=True)
24
- name: Mapped[str] = mapped_column(String(100))
25
- email: Mapped[str] = mapped_column(String(100))
26
- age: Mapped[int]
27
- grade: Mapped[str] = mapped_column(String(5))
28
- created_at : Mapped[datetime] = mapped_column(default=datetime.utcnow)
29
-
30
- class StudentCreate(BaseModel):
31
- name: str
32
- email: str
33
- age: int
34
- grade: str
35
-
36
- class StudentUpdate(BaseModel):
37
- name: Optional[str] = None
38
- email: Optional[str] = None
39
- age: Optional[int] = None
40
- grade: Optional[str] = None
41
-
42
- class StudentOut(BaseModel):
43
- id: int
44
- name: str
45
- email: str
46
- age: int
47
- grade: str
48
- created_at: datetime
49
-
50
- model_config = ConfigDict(from_attributes=True)
51
-
52
-
53
-
54
- @asynccontextmanager
55
- async def lifespan(app: FastAPI):
56
- async with engine.begin() as conn:
57
- await conn.run_sync(Base.metadata.create_all)
58
- print("Tables created !!!!")
59
- yield
60
- await engine.dispose()
61
- print("🔻 Database connection closed.")
62
-
63
- app = FastAPI(title="Async student CRUD API", lifespan=lifespan)
64
-
65
- @app.post("/students/", response_model=StudentOut, status_code=status.HTTP_201_CREATED)
66
- async def create_student(student: StudentCreate):
67
- async with async_session() as session:
68
- async with session.begin():
69
- student = Student(**student.model_dump())
70
- session.add(student)
71
- await session.refresh(student)
72
- return student
73
-
74
- @app.get("/students/", response_model=List[StudentOut])
75
- async def list_students():
76
- async with async_session() as session:
77
- result = await session.execute(select(Student))
78
- students = result.scalars().all()
79
- return students
80
-
81
- @app.get("/students/{id}", response_model=StudentOut)
82
- async def get_student(id: int):
83
- async with async_session() as session:
84
- student = await session.get(Student, id)
85
- if not student:
86
- raise HTTPException(status_code=404, detail="Student not found")
87
- return student
88
-
89
- @app.put("/students/{id}/", response_model=StudentOut)
90
- async def update_student(id: int, update_data: StudentUpdate):
91
- async with async_session() as session:
92
- async with session.begin():
93
- student = await session.get(Student, id)
94
- if not student:
95
- raise HTTPException(status_code=404, detail="student not found")
96
-
97
- update_dict = update_data.model_dump(exclude_unset=True)
98
- for key , value in update_dict.items():
99
- setattr(student, key , value)
100
-
101
- await session.refresh(student)
102
- return student
103
-
104
- @app.delete("/students/{id}", status_code=status.HTTP_204_NO_CONTENT)
105
- async def delete_student(id: int):
106
- async with async_session() as session:
107
- async with session.begin():
108
- student = await session.get(Student, id)
109
- if not student:
110
- raise HTTPException(status_code=404, detail="student not found")
111
- await session.delete(student)
112
- return None
113
-
114
-
115
- # async def main():
116
- # # await create_table()
117
- # await add_student("Akshat Mehta", "akshat@example.com", 21, "A+")
118
- # await add_student("Nikhil Sharma", "nikhil@example.com", 22, "B")
119
- # await list_students()
120
-
121
- # await update_student(1, new_age=52)
122
- # await delete_student(2)
123
- # print('-'*100)
124
- # await list_students()
125
-
126
- # if __name__ == "__main__":
127
- # asyncio.run(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/main.py DELETED
@@ -1,445 +0,0 @@
1
- """
2
- FastAPI + PostgreSQL Student Management System
3
- Complete async implementation with SQLAlchemy, authentication, and validation
4
-
5
- Requirements:
6
- pip install fastapi uvicorn sqlalchemy asyncpg psycopg2-binary python-jose[cryptography] passlib[bcrypt] python-multipart
7
- """
8
-
9
- from fastapi import FastAPI, HTTPException, Depends, status
10
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
- from fastapi.middleware.cors import CORSMiddleware
12
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
13
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
14
- from sqlalchemy import select, String
15
- from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
16
- from typing import Optional, List
17
- from datetime import datetime, timedelta
18
- from jose import JWTError, jwt
19
- from passlib.context import CryptContext
20
- import os
21
- from contextlib import asynccontextmanager
22
- # ======================== CONFIGURATION ========================
23
- DATABASE_URL = os.getenv(
24
- "DATABASE_URL",
25
- "postgresql+asyncpg://postgres:690869@172.26.157.164:5432/studentdb"
26
- )
27
- SECRET_KEY = os.getenv("SECRET_KEY", "production")
28
- ALGORITHM = "HS256"
29
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
30
-
31
- # ======================== DATABASE SETUP ========================
32
- engine = create_async_engine(DATABASE_URL, echo=True)
33
- async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
34
-
35
- class Base(DeclarativeBase):
36
- pass
37
-
38
- class Student(Base):
39
- __tablename__ = "students"
40
-
41
- id: Mapped[int] = mapped_column(primary_key=True, index=True)
42
- name: Mapped[str] = mapped_column(String(100))
43
- email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
44
- age: Mapped[int]
45
- grade: Mapped[str] = mapped_column(String(5))
46
- created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
47
-
48
- class User(Base):
49
- __tablename__ = "users"
50
-
51
- id: Mapped[int] = mapped_column(primary_key=True, index=True)
52
- username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
53
- hashed_password: Mapped[str] = mapped_column(String(255))
54
-
55
- # ======================== PYDANTIC MODELS ========================
56
- class StudentBase(BaseModel):
57
- name: str = Field(..., min_length=2, max_length=100, description="Student full name")
58
- email: EmailStr = Field(..., description="Student email address")
59
- age: int = Field(..., ge=5, le=100, description="Student age (5-100)")
60
- grade: str = Field(..., pattern="^[A-F][+-]?$", description="Grade (A-F with optional + or -)")
61
-
62
- @field_validator('name')
63
- def validate_name(cls, v):
64
- if not v.strip():
65
- raise ValueError('Name cannot be empty or just whitespace')
66
- return v.strip()
67
-
68
- @field_validator('grade')
69
- def validate_grade(cls, v):
70
- v = v.upper()
71
- if v not in ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']:
72
- raise ValueError('Invalid grade format')
73
- return v
74
-
75
- class StudentCreate(StudentBase):
76
- pass
77
-
78
- class StudentUpdate(BaseModel):
79
- name: Optional[str] = Field(None, min_length=2, max_length=100)
80
- email: Optional[EmailStr] = None
81
- age: Optional[int] = Field(None, ge=5, le=100)
82
- grade: Optional[str] = Field(None, pattern="^[A-F][+-]?$")
83
-
84
- @field_validator('grade')
85
- def validate_grade(cls, v):
86
- if v is not None:
87
- v = v.upper()
88
- if v not in ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']:
89
- raise ValueError('Invalid grade format')
90
- return v
91
-
92
- class StudentResponse(StudentBase):
93
- id: int
94
- created_at: datetime
95
-
96
- model_config = ConfigDict(from_attributes=True)
97
-
98
- class UserCreate(BaseModel):
99
- username: str = Field(..., min_length=3, max_length=50)
100
- password: str = Field(..., min_length=6, max_length=72)
101
-
102
- @field_validator('password')
103
- def validate_password(cls, v):
104
- if len(v.encode('utf-8')) > 72:
105
- raise ValueError('Password cannot exceed 72 bytes')
106
- return v
107
-
108
- class Token(BaseModel):
109
- access_token: str
110
- token_type: str
111
-
112
- # ======================== SECURITY ========================
113
- pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
114
- security = HTTPBearer()
115
-
116
- def verify_password(plain_password: str, hashed_password: str) -> bool:
117
- # Truncate password to 72 bytes for bcrypt compatibility
118
- password_bytes = plain_password.encode('utf-8')[:72]
119
- plain_password_truncated = password_bytes.decode('utf-8', errors='ignore')
120
- return pwd_context.verify(plain_password_truncated, hashed_password)
121
-
122
- def get_password_hash(password: str) -> str:
123
- # Truncate password to 72 bytes for bcrypt compatibility
124
- password_bytes = password.encode('utf-8')[:72]
125
- password_truncated = password_bytes.decode('utf-8', errors='ignore')
126
- return pwd_context.hash(password_truncated)
127
-
128
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
129
- to_encode = data.copy()
130
- expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
131
- to_encode.update({"exp": expire})
132
- encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
133
- return encoded_jwt
134
-
135
- async def get_current_user(
136
- credentials: HTTPAuthorizationCredentials = Depends(security),
137
- db: AsyncSession = Depends(lambda: async_session_maker())
138
- ):
139
- credentials_exception = HTTPException(
140
- status_code=status.HTTP_401_UNAUTHORIZED,
141
- detail="Could not validate credentials",
142
- headers={"WWW-Authenticate": "Bearer"},
143
- )
144
- try:
145
- token = credentials.credentials
146
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
147
- username: str = payload.get("sub")
148
- if username is None:
149
- raise credentials_exception
150
- except JWTError:
151
- raise credentials_exception
152
-
153
- result = await db.execute(select(User).filter(User.username == username))
154
- user = result.scalar_one_or_none()
155
- if user is None:
156
- raise credentials_exception
157
- return user
158
-
159
- # ======================== DATABASE DEPENDENCY ========================
160
- async def get_db():
161
- async with async_session_maker() as session:
162
- try:
163
- yield session
164
- await session.commit()
165
- except Exception:
166
- await session.rollback()
167
- raise
168
- finally:
169
- await session.close()
170
-
171
- # new on event alternative
172
-
173
- @asynccontextmanager
174
- async def lifespan(app: FastAPI):
175
- print("🏗️ Server starting:", datetime.now())
176
- print("🔧 Creating tables if they don't exist...")
177
-
178
- async with engine.begin() as conn:
179
- await conn.run_sync(Base.metadata.create_all) # Create tables
180
-
181
- print("✅ Tables ready!")
182
- yield
183
- print("🧹 Server shutting down:", datetime.now())
184
-
185
- # ======================== FASTAPI APP ========================
186
- app = FastAPI(
187
- title="Student Management API",
188
- description="FastAPI + PostgreSQL with SQLAlchemy async",
189
- version="1.0.0",
190
- lifespan=lifespan
191
-
192
- )
193
-
194
- # CORS Configuration
195
- app.add_middleware(
196
- CORSMiddleware,
197
- allow_origins=["*"], # In production, specify allowed origins
198
- allow_credentials=True,
199
- allow_methods=["*"],
200
- allow_headers=["*"],
201
- )
202
-
203
-
204
- # ======================== AUTHENTICATION ROUTES ========================
205
- @app.post("/auth/register", response_model=dict, tags=["Authentication"])
206
- async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
207
- """Register a new user"""
208
- try:
209
- result = await db.execute(select(User).filter(User.username == user.username))
210
- existing_user = result.scalar_one_or_none()
211
-
212
- if existing_user:
213
- raise HTTPException(
214
- status_code=status.HTTP_400_BAD_REQUEST,
215
- detail="Username already registered"
216
- )
217
-
218
- new_user = User(
219
- username=user.username,
220
- hashed_password=get_password_hash(user.password)
221
- )
222
- db.add(new_user)
223
- await db.commit()
224
-
225
- return {"message": "User registered successfully", "username": user.username}
226
- except HTTPException:
227
- raise
228
- except Exception as e:
229
- raise HTTPException(
230
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
231
- detail=f"Registration failed: {str(e)}"
232
- )
233
-
234
- @app.post("/auth/login", response_model=Token, tags=["Authentication"])
235
- async def login(username: str, password: str, db: AsyncSession = Depends(get_db)):
236
- """Login and get access token"""
237
- try:
238
- result = await db.execute(select(User).filter(User.username == username))
239
- user = result.scalar_one_or_none()
240
-
241
- if not user or not verify_password(password, user.hashed_password):
242
- raise HTTPException(
243
- status_code=status.HTTP_401_UNAUTHORIZED,
244
- detail="Incorrect username or password",
245
- headers={"WWW-Authenticate": "Bearer"},
246
- )
247
-
248
- access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
249
- access_token = create_access_token(
250
- data={"sub": user.username},
251
- expires_delta=access_token_expires
252
- )
253
-
254
- return {"access_token": access_token, "token_type": "bearer"}
255
- except HTTPException:
256
- raise
257
- except Exception as e:
258
- raise HTTPException(
259
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
260
- detail=f"Login failed: {str(e)}"
261
- )
262
-
263
- # ======================== STUDENT CRUD ROUTES ========================
264
- @app.post("/students/", response_model=StudentResponse, status_code=status.HTTP_201_CREATED, tags=["Students"])
265
- async def create_student(
266
- student: StudentCreate,
267
- db: AsyncSession = Depends(get_db),
268
- current_user: User = Depends(get_current_user)
269
- ):
270
- """Create a new student (Protected)"""
271
- try:
272
- # Check if email already exists
273
- result = await db.execute(select(Student).filter(Student.email == student.email))
274
- existing_student = result.scalar_one_or_none()
275
-
276
- if existing_student:
277
- raise HTTPException(
278
- status_code=status.HTTP_400_BAD_REQUEST,
279
- detail=f"Student with email {student.email} already exists"
280
- )
281
-
282
- new_student = Student(**student.model_dump())
283
- db.add(new_student)
284
- await db.commit()
285
- await db.refresh(new_student)
286
-
287
- return new_student
288
- except HTTPException:
289
- raise
290
- except Exception as e:
291
- raise HTTPException(
292
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
293
- detail=f"Failed to create student: {str(e)}"
294
- )
295
-
296
- @app.get("/students/", response_model=List[StudentResponse], tags=["Students"])
297
- async def get_all_students(
298
- skip: int = 0,
299
- limit: int = 100,
300
- db: AsyncSession = Depends(get_db),
301
- current_user: User = Depends(get_current_user)
302
- ):
303
- """Get all students with pagination (Protected)"""
304
- try:
305
- result = await db.execute(
306
- select(Student).offset(skip).limit(limit)
307
- )
308
- students = result.scalars().all()
309
- return students
310
- except Exception as e:
311
- raise HTTPException(
312
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
313
- detail=f"Failed to fetch students: {str(e)}"
314
- )
315
-
316
- @app.get("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
317
- async def get_student(
318
- student_id: int,
319
- db: AsyncSession = Depends(get_db),
320
- current_user: User = Depends(get_current_user)
321
- ):
322
- """Get a specific student by ID (Protected)"""
323
- try:
324
- result = await db.execute(select(Student).filter(Student.id == student_id))
325
- student = result.scalar_one_or_none()
326
-
327
- if not student:
328
- raise HTTPException(
329
- status_code=status.HTTP_404_NOT_FOUND,
330
- detail=f"Student with ID {student_id} not found"
331
- )
332
-
333
- return student
334
- except HTTPException:
335
- raise
336
- except Exception as e:
337
- raise HTTPException(
338
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
339
- detail=f"Failed to fetch student: {str(e)}"
340
- )
341
-
342
- @app.put("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
343
- async def update_student(
344
- student_id: int,
345
- student_update: StudentUpdate,
346
- db: AsyncSession = Depends(get_db),
347
- current_user: User = Depends(get_current_user)
348
- ):
349
- """Update a student's information (Protected)"""
350
- try:
351
- result = await db.execute(select(Student).filter(Student.id == student_id))
352
- student = result.scalar_one_or_none()
353
-
354
- if not student:
355
- raise HTTPException(
356
- status_code=status.HTTP_404_NOT_FOUND,
357
- detail=f"Student with ID {student_id} not found"
358
- )
359
-
360
- # Update only provided fields
361
- update_data = student_update.model_dump(exclude_unset=True)
362
-
363
- # Check email uniqueness if email is being updated
364
- if "email" in update_data:
365
- result = await db.execute(
366
- select(Student).filter(
367
- Student.email == update_data["email"],
368
- Student.id != student_id
369
- )
370
- )
371
- existing = result.scalar_one_or_none()
372
- if existing:
373
- raise HTTPException(
374
- status_code=status.HTTP_400_BAD_REQUEST,
375
- detail=f"Email {update_data['email']} is already in use"
376
- )
377
-
378
- for key, value in update_data.items():
379
- setattr(student, key, value)
380
-
381
- await db.commit()
382
- await db.refresh(student)
383
-
384
- return student
385
- except HTTPException:
386
- raise
387
- except Exception as e:
388
- raise HTTPException(
389
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
390
- detail=f"Failed to update student: {str(e)}"
391
- )
392
-
393
- @app.patch("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
394
- async def partial_update_student(
395
- student_id: int,
396
- student_update: StudentUpdate,
397
- db: AsyncSession = Depends(get_db),
398
- current_user: User = Depends(get_current_user)
399
- ):
400
- """Partially update a student (same as PUT for this implementation) (Protected)"""
401
- return await update_student(student_id, student_update, db, current_user)
402
-
403
- @app.delete("/students/{student_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Students"])
404
- async def delete_student(
405
- student_id: int,
406
- db: AsyncSession = Depends(get_db),
407
- current_user: User = Depends(get_current_user)
408
- ):
409
- """Delete a student (Protected)"""
410
- try:
411
- result = await db.execute(select(Student).filter(Student.id == student_id))
412
- student = result.scalar_one_or_none()
413
-
414
- if not student:
415
- raise HTTPException(
416
- status_code=status.HTTP_404_NOT_FOUND,
417
- detail=f"Student with ID {student_id} not found"
418
- )
419
-
420
- await db.delete(student)
421
- await db.commit()
422
-
423
- return None
424
- except HTTPException:
425
- raise
426
- except Exception as e:
427
- raise HTTPException(
428
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
429
- detail=f"Failed to delete student: {str(e)}"
430
- )
431
-
432
- # ======================== HEALTH CHECK ========================
433
- @app.get("/", tags=["Health"])
434
- async def root():
435
- """Health check endpoint"""
436
- return {
437
- "status": "healthy",
438
- "message": "Student Management API is running",
439
- "version": "1.0.0"
440
- }
441
-
442
- # ======================== RUN APPLICATION ========================
443
- if __name__ == "__main__":
444
- import uvicorn
445
- uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/test.ipynb DELETED
@@ -1,189 +0,0 @@
1
- {
2
- "cells": [
3
- {
4
- "cell_type": "code",
5
- "execution_count": 1,
6
- "id": "d0e4e192",
7
- "metadata": {},
8
- "outputs": [],
9
- "source": [
10
- "import asyncio\n",
11
- "from datetime import datetime\n",
12
- "from sqlalchemy import String, Integer, DateTime\n",
13
- "from sqlalchemy.ext.asyncio import create_async_engine, async_sessionmaker, AsyncSession\n",
14
- "from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\n",
15
- "from sqlalchemy import select\n",
16
- "\n",
17
- "DATABASE_URL = \"postgresql+asyncpg://postgres:690869@localhost:5432/studentdb\"\n",
18
- "\n",
19
- "engine = create_async_engine(DATABASE_URL, echo=True)\n",
20
- "async_session = async_sessionmaker(engine, expire_on_commit=False)\n",
21
- "\n",
22
- "# class Base(DeclarativeBase):\n",
23
- "# pass\n",
24
- "Base = DeclarativeBase()"
25
- ]
26
- },
27
- {
28
- "cell_type": "code",
29
- "execution_count": 4,
30
- "id": "f2813d66",
31
- "metadata": {},
32
- "outputs": [
33
- {
34
- "data": {
35
- "text/plain": [
36
- "<sqlalchemy.orm.decl_api.DeclarativeBase at 0x7df42272b620>"
37
- ]
38
- },
39
- "execution_count": 4,
40
- "metadata": {},
41
- "output_type": "execute_result"
42
- }
43
- ],
44
- "source": [
45
- "Base\n"
46
- ]
47
- },
48
- {
49
- "cell_type": "code",
50
- "execution_count": null,
51
- "id": "52154570",
52
- "metadata": {},
53
- "outputs": [
54
- {
55
- "ename": "ValidationError",
56
- "evalue": "4 validation errors for Quiz_input\nparsed_doc\n Field required [type=missing, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing\nuser_prompt\n Field required [type=missing, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing\nmcq_choice\n Field required [type=missing, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing\ncode_choice\n Field required [type=missing, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing",
57
- "output_type": "error",
58
- "traceback": [
59
- "\u001b[31m---------------------------------------------------------------------------\u001b[39m",
60
- "\u001b[31mValidationError\u001b[39m Traceback (most recent call last)",
61
- "\u001b[36mCell\u001b[39m\u001b[36m \u001b[39m\u001b[32mIn[3]\u001b[39m\u001b[32m, line 6\u001b[39m\n\u001b[32m 3\u001b[39m \u001b[38;5;28;01mdef\u001b[39;00m\u001b[38;5;250m \u001b[39m\u001b[34mlength\u001b[39m(\u001b[38;5;28minput\u001b[39m: Quiz_input):\n\u001b[32m 4\u001b[39m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mlen\u001b[39m(\u001b[38;5;28minput\u001b[39m)\n\u001b[32m----> \u001b[39m\u001b[32m6\u001b[39m data = \u001b[43mQuiz_input\u001b[49m\u001b[43m(\u001b[49m\u001b[43m)\u001b[49m\n\u001b[32m 8\u001b[39m data.parsed_doc = \u001b[33m\"\u001b[39m\u001b[33mhello\u001b[39m\u001b[33m\"\u001b[39m\n\u001b[32m 9\u001b[39m result = length(data)\n",
62
- "\u001b[36mFile \u001b[39m\u001b[32m~/miniforge3/envs/prep/lib/python3.13/site-packages/pydantic/main.py:250\u001b[39m, in \u001b[36mBaseModel.__init__\u001b[39m\u001b[34m(self, **data)\u001b[39m\n\u001b[32m 248\u001b[39m \u001b[38;5;66;03m# `__tracebackhide__` tells pytest and some other tools to omit this function from tracebacks\u001b[39;00m\n\u001b[32m 249\u001b[39m __tracebackhide__ = \u001b[38;5;28;01mTrue\u001b[39;00m\n\u001b[32m--> \u001b[39m\u001b[32m250\u001b[39m validated_self = \u001b[38;5;28;43mself\u001b[39;49m\u001b[43m.\u001b[49m\u001b[43m__pydantic_validator__\u001b[49m\u001b[43m.\u001b[49m\u001b[43mvalidate_python\u001b[49m\u001b[43m(\u001b[49m\u001b[43mdata\u001b[49m\u001b[43m,\u001b[49m\u001b[43m \u001b[49m\u001b[43mself_instance\u001b[49m\u001b[43m=\u001b[49m\u001b[38;5;28;43mself\u001b[39;49m\u001b[43m)\u001b[49m\n\u001b[32m 251\u001b[39m \u001b[38;5;28;01mif\u001b[39;00m \u001b[38;5;28mself\u001b[39m \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;129;01mnot\u001b[39;00m validated_self:\n\u001b[32m 252\u001b[39m warnings.warn(\n\u001b[32m 253\u001b[39m \u001b[33m'\u001b[39m\u001b[33mA custom validator is returning a value other than `self`.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m'\u001b[39m\n\u001b[32m 254\u001b[39m \u001b[33m\"\u001b[39m\u001b[33mReturning anything other than `self` from a top level model validator isn\u001b[39m\u001b[33m'\u001b[39m\u001b[33mt supported when validating via `__init__`.\u001b[39m\u001b[38;5;130;01m\\n\u001b[39;00m\u001b[33m\"\u001b[39m\n\u001b[32m 255\u001b[39m \u001b[33m'\u001b[39m\u001b[33mSee the `model_validator` docs (https://docs.pydantic.dev/latest/concepts/validators/#model-validators) for more details.\u001b[39m\u001b[33m'\u001b[39m,\n\u001b[32m 256\u001b[39m stacklevel=\u001b[32m2\u001b[39m,\n\u001b[32m 257\u001b[39m )\n",
63
- "\u001b[31mValidationError\u001b[39m: 4 validation errors for Quiz_input\nparsed_doc\n Field required [type=missing, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing\nuser_prompt\n Field required [type=missing, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing\nmcq_choice\n Field required [type=missing, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing\ncode_choice\n Field required [type=missing, input_value={}, input_type=dict]\n For further information visit https://errors.pydantic.dev/2.12/v/missing"
64
- ]
65
- }
66
- ],
67
- "source": [
68
- "from app.schema.models import Quiz_input\n",
69
- "\n",
70
- "def length(input: Quiz_input):\n",
71
- " return len(input)\n",
72
- "\n",
73
- "data = Quiz_input()\n",
74
- "hello = \"hello\"\n",
75
- "data.parsed_doc = hello\n",
76
- "result = length(data)\n",
77
- "print(result)"
78
- ]
79
- },
80
- {
81
- "cell_type": "code",
82
- "execution_count": 2,
83
- "id": "f0dac635",
84
- "metadata": {},
85
- "outputs": [
86
- {
87
- "data": {
88
- "text/plain": [
89
- "{'parsed_doc': 'hello'}"
90
- ]
91
- },
92
- "execution_count": 2,
93
- "metadata": {},
94
- "output_type": "execute_result"
95
- }
96
- ],
97
- "source": [
98
- "data"
99
- ]
100
- },
101
- {
102
- "cell_type": "code",
103
- "execution_count": 2,
104
- "id": "b964b9c3",
105
- "metadata": {},
106
- "outputs": [],
107
- "source": [
108
- "import json\n",
109
- "\n",
110
- "with open(\"1000-data-science-questions-answers.json\", \"r\") as f:\n",
111
- " data = json.load(f)\n"
112
- ]
113
- },
114
- {
115
- "cell_type": "code",
116
- "execution_count": 4,
117
- "id": "f2579e63",
118
- "metadata": {},
119
- "outputs": [
120
- {
121
- "data": {
122
- "text/plain": [
123
- "500"
124
- ]
125
- },
126
- "execution_count": 4,
127
- "metadata": {},
128
- "output_type": "execute_result"
129
- }
130
- ],
131
- "source": [
132
- "len(data)"
133
- ]
134
- },
135
- {
136
- "cell_type": "code",
137
- "execution_count": 6,
138
- "id": "005c0e97",
139
- "metadata": {},
140
- "outputs": [
141
- {
142
- "data": {
143
- "text/plain": [
144
- "{'question': 'Which of the following CLI command can also be used to rename files?',\n",
145
- " 'options': ['rm', 'mv', 'rm -r', 'none of the mentioned'],\n",
146
- " 'answer': 'b',\n",
147
- " 'explanation': 'Explanation: mv stands for move.'}"
148
- ]
149
- },
150
- "execution_count": 6,
151
- "metadata": {},
152
- "output_type": "execute_result"
153
- }
154
- ],
155
- "source": [
156
- "data[0]"
157
- ]
158
- },
159
- {
160
- "cell_type": "code",
161
- "execution_count": null,
162
- "id": "8222408a",
163
- "metadata": {},
164
- "outputs": [],
165
- "source": []
166
- }
167
- ],
168
- "metadata": {
169
- "kernelspec": {
170
- "display_name": "prep",
171
- "language": "python",
172
- "name": "python3"
173
- },
174
- "language_info": {
175
- "codemirror_mode": {
176
- "name": "ipython",
177
- "version": 3
178
- },
179
- "file_extension": ".py",
180
- "mimetype": "text/x-python",
181
- "name": "python",
182
- "nbconvert_exporter": "python",
183
- "pygments_lexer": "ipython3",
184
- "version": "3.13.9"
185
- }
186
- },
187
- "nbformat": 4,
188
- "nbformat_minor": 5
189
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/test.py DELETED
@@ -1,445 +0,0 @@
1
- """
2
- FastAPI + PostgreSQL Student Management System
3
- Complete async implementation with SQLAlchemy, authentication, and validation
4
-
5
- Requirements:
6
- pip install fastapi uvicorn sqlalchemy asyncpg psycopg2-binary python-jose[cryptography] passlib[bcrypt] python-multipart
7
- """
8
-
9
- from fastapi import FastAPI, HTTPException, Depends, status
10
- from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
11
- from fastapi.middleware.cors import CORSMiddleware
12
- from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
13
- from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
14
- from sqlalchemy import select, String
15
- from pydantic import BaseModel, EmailStr, Field, field_validator, ConfigDict
16
- from typing import Optional, List
17
- from datetime import datetime, timedelta
18
- from jose import JWTError, jwt
19
- from passlib.context import CryptContext
20
- import os
21
- from contextlib import asynccontextmanager
22
- # ======================== CONFIGURATION ========================
23
- DATABASE_URL = os.getenv(
24
- "DATABASE_URL",
25
- "postgresql+asyncpg://postgres:690869@172.26.157.164:5432/studentdb"
26
- )
27
- SECRET_KEY = os.getenv("SECRET_KEY", "production")
28
- ALGORITHM = "HS256"
29
- ACCESS_TOKEN_EXPIRE_MINUTES = 30
30
-
31
- # ======================== DATABASE SETUP ========================
32
- engine = create_async_engine(DATABASE_URL, echo=True)
33
- async_session_maker = async_sessionmaker(engine, expire_on_commit=False)
34
-
35
- class Base(DeclarativeBase):
36
- pass
37
-
38
- class Student(Base):
39
- __tablename__ = "students"
40
-
41
- id: Mapped[int] = mapped_column(primary_key=True, index=True)
42
- name: Mapped[str] = mapped_column(String(100))
43
- email: Mapped[str] = mapped_column(String(100), unique=True, index=True)
44
- age: Mapped[int]
45
- grade: Mapped[str] = mapped_column(String(5))
46
- created_at: Mapped[datetime] = mapped_column(default=datetime.utcnow)
47
-
48
- class User(Base):
49
- __tablename__ = "users"
50
-
51
- id: Mapped[int] = mapped_column(primary_key=True, index=True)
52
- username: Mapped[str] = mapped_column(String(50), unique=True, index=True)
53
- hashed_password: Mapped[str] = mapped_column(String(255))
54
-
55
- # ======================== PYDANTIC MODELS ========================
56
- class StudentBase(BaseModel):
57
- name: str = Field(..., min_length=2, max_length=100, description="Student full name")
58
- email: EmailStr = Field(..., description="Student email address")
59
- age: int = Field(..., ge=5, le=100, description="Student age (5-100)")
60
- grade: str = Field(..., pattern="^[A-F][+-]?$", description="Grade (A-F with optional + or -)")
61
-
62
- @field_validator('name')
63
- def validate_name(cls, v):
64
- if not v.strip():
65
- raise ValueError('Name cannot be empty or just whitespace')
66
- return v.strip()
67
-
68
- @field_validator('grade')
69
- def validate_grade(cls, v):
70
- v = v.upper()
71
- if v not in ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']:
72
- raise ValueError('Invalid grade format')
73
- return v
74
-
75
- class StudentCreate(StudentBase):
76
- pass
77
-
78
- class StudentUpdate(BaseModel):
79
- name: Optional[str] = Field(None, min_length=2, max_length=100)
80
- email: Optional[EmailStr] = None
81
- age: Optional[int] = Field(None, ge=5, le=100)
82
- grade: Optional[str] = Field(None, pattern="^[A-F][+-]?$")
83
-
84
- @field_validator('grade')
85
- def validate_grade(cls, v):
86
- if v is not None:
87
- v = v.upper()
88
- if v not in ['A+', 'A', 'A-', 'B+', 'B', 'B-', 'C+', 'C', 'C-', 'D+', 'D', 'D-', 'F']:
89
- raise ValueError('Invalid grade format')
90
- return v
91
-
92
- class StudentResponse(StudentBase):
93
- id: int
94
- created_at: datetime
95
-
96
- model_config = ConfigDict(from_attributes=True)
97
-
98
- class UserCreate(BaseModel):
99
- username: str = Field(..., min_length=3, max_length=50)
100
- password: str = Field(..., min_length=6, max_length=72)
101
-
102
- @field_validator('password')
103
- def validate_password(cls, v):
104
- if len(v.encode('utf-8')) > 72:
105
- raise ValueError('Password cannot exceed 72 bytes')
106
- return v
107
-
108
- class Token(BaseModel):
109
- access_token: str
110
- token_type: str
111
-
112
- # ======================== SECURITY ========================
113
- pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
114
- security = HTTPBearer()
115
-
116
- def verify_password(plain_password: str, hashed_password: str) -> bool:
117
- # Truncate password to 72 bytes for bcrypt compatibility
118
- password_bytes = plain_password.encode('utf-8')[:72]
119
- plain_password_truncated = password_bytes.decode('utf-8', errors='ignore')
120
- return pwd_context.verify(plain_password_truncated, hashed_password)
121
-
122
- def get_password_hash(password: str) -> str:
123
- # Truncate password to 72 bytes for bcrypt compatibility
124
- password_bytes = password.encode('utf-8')[:72]
125
- password_truncated = password_bytes.decode('utf-8', errors='ignore')
126
- return pwd_context.hash(password_truncated)
127
-
128
- def create_access_token(data: dict, expires_delta: Optional[timedelta] = None):
129
- to_encode = data.copy()
130
- expire = datetime.utcnow() + (expires_delta or timedelta(minutes=15))
131
- to_encode.update({"exp": expire})
132
- encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
133
- return encoded_jwt
134
-
135
- async def get_current_user(
136
- credentials: HTTPAuthorizationCredentials = Depends(security),
137
- db: AsyncSession = Depends(lambda: async_session_maker())
138
- ):
139
- credentials_exception = HTTPException(
140
- status_code=status.HTTP_401_UNAUTHORIZED,
141
- detail="Could not validate credentials",
142
- headers={"WWW-Authenticate": "Bearer"},
143
- )
144
- try:
145
- token = credentials.credentials
146
- payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
147
- username: str = payload.get("sub")
148
- if username is None:
149
- raise credentials_exception
150
- except JWTError:
151
- raise credentials_exception
152
-
153
- result = await db.execute(select(User).filter(User.username == username))
154
- user = result.scalar_one_or_none()
155
- if user is None:
156
- raise credentials_exception
157
- return user
158
-
159
- # ======================== DATABASE DEPENDENCY ========================
160
- async def get_db():
161
- async with async_session_maker() as session:
162
- try:
163
- yield session
164
- await session.commit()
165
- except Exception:
166
- await session.rollback()
167
- raise
168
- finally:
169
- await session.close()
170
-
171
- # new on event alternative
172
-
173
- @asynccontextmanager
174
- async def lifespan(app: FastAPI):
175
- print("🏗️ Server starting:", datetime.now())
176
- print("🔧 Creating tables if they don't exist...")
177
-
178
- async with engine.begin() as conn:
179
- await conn.run_sync(Base.metadata.create_all) # Create tables
180
-
181
- print("✅ Tables ready!")
182
- yield
183
- print("🧹 Server shutting down:", datetime.now())
184
-
185
- # ======================== FASTAPI APP ========================
186
- app = FastAPI(
187
- title="Student Management API",
188
- description="FastAPI + PostgreSQL with SQLAlchemy async",
189
- version="1.0.0",
190
- lifespan=lifespan
191
-
192
- )
193
-
194
- # CORS Configuration
195
- app.add_middleware(
196
- CORSMiddleware,
197
- allow_origins=["*"], # In production, specify allowed origins
198
- allow_credentials=True,
199
- allow_methods=["*"],
200
- allow_headers=["*"],
201
- )
202
-
203
-
204
- # ======================== AUTHENTICATION ROUTES ========================
205
- @app.post("/auth/register", response_model=dict, tags=["Authentication"])
206
- async def register(user: UserCreate, db: AsyncSession = Depends(get_db)):
207
- """Register a new user"""
208
- try:
209
- result = await db.execute(select(User).filter(User.username == user.username))
210
- existing_user = result.scalar_one_or_none()
211
-
212
- if existing_user:
213
- raise HTTPException(
214
- status_code=status.HTTP_400_BAD_REQUEST,
215
- detail="Username already registered"
216
- )
217
-
218
- new_user = User(
219
- username=user.username,
220
- hashed_password=get_password_hash(user.password)
221
- )
222
- db.add(new_user)
223
- await db.commit()
224
-
225
- return {"message": "User registered successfully", "username": user.username}
226
- except HTTPException:
227
- raise
228
- except Exception as e:
229
- raise HTTPException(
230
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
231
- detail=f"Registration failed: {str(e)}"
232
- )
233
-
234
- @app.post("/auth/login", response_model=Token, tags=["Authentication"])
235
- async def login(username: str, password: str, db: AsyncSession = Depends(get_db)):
236
- """Login and get access token"""
237
- try:
238
- result = await db.execute(select(User).filter(User.username == username))
239
- user = result.scalar_one_or_none()
240
-
241
- if not user or not verify_password(password, user.hashed_password):
242
- raise HTTPException(
243
- status_code=status.HTTP_401_UNAUTHORIZED,
244
- detail="Incorrect username or password",
245
- headers={"WWW-Authenticate": "Bearer"},
246
- )
247
-
248
- access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
249
- access_token = create_access_token(
250
- data={"sub": user.username},
251
- expires_delta=access_token_expires
252
- )
253
-
254
- return {"access_token": access_token, "token_type": "bearer"}
255
- except HTTPException:
256
- raise
257
- except Exception as e:
258
- raise HTTPException(
259
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
260
- detail=f"Login failed: {str(e)}"
261
- )
262
-
263
- # ======================== STUDENT CRUD ROUTES ========================
264
- @app.post("/students/", response_model=StudentResponse, status_code=status.HTTP_201_CREATED, tags=["Students"])
265
- async def create_student(
266
- student: StudentCreate,
267
- db: AsyncSession = Depends(get_db),
268
- current_user: User = Depends(get_current_user)
269
- ):
270
- """Create a new student (Protected)"""
271
- try:
272
- # Check if email already exists
273
- result = await db.execute(select(Student).filter(Student.email == student.email))
274
- existing_student = result.scalar_one_or_none()
275
-
276
- if existing_student:
277
- raise HTTPException(
278
- status_code=status.HTTP_400_BAD_REQUEST,
279
- detail=f"Student with email {student.email} already exists"
280
- )
281
-
282
- new_student = Student(**student.model_dump())
283
- db.add(new_student)
284
- await db.commit()
285
- await db.refresh(new_student)
286
-
287
- return new_student
288
- except HTTPException:
289
- raise
290
- except Exception as e:
291
- raise HTTPException(
292
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
293
- detail=f"Failed to create student: {str(e)}"
294
- )
295
-
296
- @app.get("/students/", response_model=List[StudentResponse], tags=["Students"])
297
- async def get_all_students(
298
- skip: int = 0,
299
- limit: int = 100,
300
- db: AsyncSession = Depends(get_db),
301
- current_user: User = Depends(get_current_user)
302
- ):
303
- """Get all students with pagination (Protected)"""
304
- try:
305
- result = await db.execute(
306
- select(Student).offset(skip).limit(limit)
307
- )
308
- students = result.scalars().all()
309
- return students
310
- except Exception as e:
311
- raise HTTPException(
312
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
313
- detail=f"Failed to fetch students: {str(e)}"
314
- )
315
-
316
- @app.get("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
317
- async def get_student(
318
- student_id: int,
319
- db: AsyncSession = Depends(get_db),
320
- current_user: User = Depends(get_current_user)
321
- ):
322
- """Get a specific student by ID (Protected)"""
323
- try:
324
- result = await db.execute(select(Student).filter(Student.id == student_id))
325
- student = result.scalar_one_or_none()
326
-
327
- if not student:
328
- raise HTTPException(
329
- status_code=status.HTTP_404_NOT_FOUND,
330
- detail=f"Student with ID {student_id} not found"
331
- )
332
-
333
- return student
334
- except HTTPException:
335
- raise
336
- except Exception as e:
337
- raise HTTPException(
338
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
339
- detail=f"Failed to fetch student: {str(e)}"
340
- )
341
-
342
- @app.put("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
343
- async def update_student(
344
- student_id: int,
345
- student_update: StudentUpdate,
346
- db: AsyncSession = Depends(get_db),
347
- current_user: User = Depends(get_current_user)
348
- ):
349
- """Update a student's information (Protected)"""
350
- try:
351
- result = await db.execute(select(Student).filter(Student.id == student_id))
352
- student = result.scalar_one_or_none()
353
-
354
- if not student:
355
- raise HTTPException(
356
- status_code=status.HTTP_404_NOT_FOUND,
357
- detail=f"Student with ID {student_id} not found"
358
- )
359
-
360
- # Update only provided fields
361
- update_data = student_update.model_dump(exclude_unset=True)
362
-
363
- # Check email uniqueness if email is being updated
364
- if "email" in update_data:
365
- result = await db.execute(
366
- select(Student).filter(
367
- Student.email == update_data["email"],
368
- Student.id != student_id
369
- )
370
- )
371
- existing = result.scalar_one_or_none()
372
- if existing:
373
- raise HTTPException(
374
- status_code=status.HTTP_400_BAD_REQUEST,
375
- detail=f"Email {update_data['email']} is already in use"
376
- )
377
-
378
- for key, value in update_data.items():
379
- setattr(student, key, value)
380
-
381
- await db.commit()
382
- await db.refresh(student)
383
-
384
- return student
385
- except HTTPException:
386
- raise
387
- except Exception as e:
388
- raise HTTPException(
389
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
390
- detail=f"Failed to update student: {str(e)}"
391
- )
392
-
393
- @app.patch("/students/{student_id}", response_model=StudentResponse, tags=["Students"])
394
- async def partial_update_student(
395
- student_id: int,
396
- student_update: StudentUpdate,
397
- db: AsyncSession = Depends(get_db),
398
- current_user: User = Depends(get_current_user)
399
- ):
400
- """Partially update a student (same as PUT for this implementation) (Protected)"""
401
- return await update_student(student_id, student_update, db, current_user)
402
-
403
- @app.delete("/students/{student_id}", status_code=status.HTTP_204_NO_CONTENT, tags=["Students"])
404
- async def delete_student(
405
- student_id: int,
406
- db: AsyncSession = Depends(get_db),
407
- current_user: User = Depends(get_current_user)
408
- ):
409
- """Delete a student (Protected)"""
410
- try:
411
- result = await db.execute(select(Student).filter(Student.id == student_id))
412
- student = result.scalar_one_or_none()
413
-
414
- if not student:
415
- raise HTTPException(
416
- status_code=status.HTTP_404_NOT_FOUND,
417
- detail=f"Student with ID {student_id} not found"
418
- )
419
-
420
- await db.delete(student)
421
- await db.commit()
422
-
423
- return None
424
- except HTTPException:
425
- raise
426
- except Exception as e:
427
- raise HTTPException(
428
- status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
429
- detail=f"Failed to delete student: {str(e)}"
430
- )
431
-
432
- # ======================== HEALTH CHECK ========================
433
- @app.get("/", tags=["Health"])
434
- async def root():
435
- """Health check endpoint"""
436
- return {
437
- "status": "healthy",
438
- "message": "Student Management API is running",
439
- "version": "1.0.0"
440
- }
441
-
442
- # ======================== RUN APPLICATION ========================
443
- if __name__ == "__main__":
444
- import uvicorn
445
- uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
test/voice.py DELETED
@@ -1,62 +0,0 @@
1
- import sounddevice as sd
2
- import numpy as np
3
- from faster_whisper import WhisperModel
4
- import ollama
5
- import subprocess
6
- import time
7
-
8
- # --- Configuration ---
9
- # STT Model: 'tiny' or 'base' are fast; 'small' is accurate.
10
- stt_model = WhisperModel("base.en", device="cuda", compute_type="float16")
11
-
12
- def record_audio(duration=5, samplerate=16000):
13
- """Simple recorder - in production, use VAD (Voice Activity Detection) to stop automatically."""
14
- print("Listening...")
15
- recording = sd.rec(int(duration * samplerate), samplerate=samplerate, channels=1, dtype='float32')
16
- sd.wait()
17
- return recording.flatten()
18
-
19
- def transcribe(audio_data):
20
- """Convert Audio to Text"""
21
- segments, info = stt_model.transcribe(audio_data, beam_size=5)
22
- text = " ".join([segment.text for segment in segments])
23
- return text.strip()
24
-
25
- def generate_response(prompt):
26
- """Send text to Ollama (The Brain)"""
27
- response = ollama.chat(model='llama3.2', messages=[
28
- {'role': 'user', 'content': prompt},
29
- ])
30
- return response['message']['content']
31
-
32
- def speak(text):
33
- """Convert Text to Audio (The Mouth) using Piper"""
34
- # Assuming you have the piper binary and a voice model downloaded
35
- # Command line: echo "text" | piper --model en_US-lessac-medium.onnx --output_file output.wav
36
-
37
- # For this example, we'll use a generic speak command for macOS/Linux
38
- # (Replace with Piper or pyttsx3 for true local/cross-platform)
39
- subprocess.call(["say", text])
40
-
41
- # --- Main Loop ---
42
- def main():
43
- print("Voice Agent Started (Ctrl+C to stop)")
44
- while True:
45
- # 1. Listen
46
- audio_data = record_audio(duration=4) # Fixed duration for simplicity
47
-
48
- # 2. Transcribe
49
- user_text = transcribe(audio_data)
50
- if not user_text: continue
51
-
52
- print(f"You: {user_text}")
53
-
54
- # 3. Think
55
- ai_response = generate_response(user_text)
56
- print(f"AI: {ai_response}")
57
-
58
- # 4. Speak
59
- speak(ai_response)
60
-
61
- if __name__ == "__main__":
62
- main()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
vector_db/1000-data-science-questions-answers.json ADDED
The diff for this file is too large to render. See raw diff