Shevilll commited on
Commit
5fde056
Β·
0 Parent(s):

Implemented the first version of the application, handled the UI, face recognition, and database handling

Browse files
Files changed (9) hide show
  1. Dockerfile +36 -0
  2. README.md +64 -0
  3. __pycache__/main.cpython-313.pyc +0 -0
  4. init_db.py +56 -0
  5. main.py +317 -0
  6. requirements.txt +11 -0
  7. test_core_flows.py +132 -0
  8. test_db.py +14 -0
  9. test_face.jpg +0 -0
Dockerfile ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Use the official Python 3.10 image as a base
2
+ FROM python:3.10-slim
3
+
4
+ # Install necessary system dependencies for OpenCV and other heavy ML tools
5
+ # DeepFace and OpenCV require libgl1 and libglib2.0
6
+ RUN apt-get update && apt-get install -y \
7
+ libgl1-mesa-glx \
8
+ libglib2.0-0 \
9
+ && rm -rf /var/lib/apt/lists/*
10
+
11
+ # Set the working directory to /app
12
+ WORKDIR /app
13
+
14
+ # Copy requirement files first to cache dependencies correctly
15
+ COPY requirements.txt .
16
+
17
+ # Install dependencies using pip
18
+ RUN pip install --no-cache-dir -r requirements.txt
19
+
20
+ # Create a non-root user required by Hugging Face Spaces environment
21
+ # Since HF Spaces run on port 7860 by default under non-root
22
+ RUN useradd -m -u 1000 user
23
+ USER user
24
+ ENV HOME=/home/user \
25
+ PATH=/home/user/.local/bin:$PATH
26
+
27
+ WORKDIR $HOME/app
28
+
29
+ # Copy the rest of the application files in the directory
30
+ COPY --chown=user . $HOME/app
31
+
32
+ # Hugging Face Spaces expose port 7860 by default
33
+ EXPOSE 7860
34
+
35
+ # Run uvicorn on port 7860, bound to 0.0.0.0
36
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md ADDED
@@ -0,0 +1,64 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VisionAttend Backend
2
+
3
+ The backend of `VisionAttend` handles the computational heavy lifting. It exposes the API endpoints necessary for student management and uses a DeepFace model for instantaneous facial embedding extraction.
4
+
5
+ ## Features
6
+
7
+ - **FastAPI Core**: Utilizes asynchronous endpoints for maximum concurrency.
8
+ - **DeepFace Integration**: Leverages the `Facenet` model for robust 128-dimensional encodings. Uses the `opencv` detector backend for rapid alignment performance compared to `retinaface` or `mtcnn`.
9
+ - **Postgres pgvector Matchmaking**: Embeddings are stored in Neon Postgres using `pgvector`. A student is recognized instantly using the `<=>` cosine distance operator to find the closest match under a set confidence threshold (0.40).
10
+
11
+ ## Requirements
12
+
13
+ Before starting, ensure you have a standard Python 3.10+ environment.
14
+
15
+ - `fastapi`
16
+ - `uvicorn`
17
+ - `deepface`
18
+ - `psycopg` (with pool and pgvector integration)
19
+ - `python-dotenv`
20
+ - `numpy` & `pillow`
21
+
22
+ ## Setup & Running
23
+
24
+ 1. **Virtual Environment Setup**
25
+ ```bash
26
+ python -m venv venv
27
+ source venv/bin/activate
28
+ pip install -r requirements.txt
29
+ ```
30
+
31
+ 2. **Environment Configuration**
32
+ Create a `.env` file referencing your Neon Postgres database containing the `pgvector` extension.
33
+ ```env
34
+ DATABASE_URL="postgres://user:password@host/dbname?sslmode=require"
35
+ ```
36
+
37
+ 3. **Initialize the Database**
38
+ Before running the app, execute the init script to build the exact table architecture needed.
39
+ ```bash
40
+ python init_db.py
41
+ ```
42
+
43
+ 4. **Launch Server**
44
+ ```bash
45
+ uvicorn main:app --reload --port 8000
46
+ ```
47
+
48
+ ## REST API Overview
49
+
50
+ All routes accept appropriate `multipart/form-data` for image payloads or generic `application/json` strings.
51
+
52
+ - `POST /api/register`: Stores a new student photo representation.
53
+ - `POST /api/recognize`: Validates a live frame against existing students and marks attendance.
54
+ - `GET /api/attendance`: Retrieves attendance logs, optionally filtered by `?date_filter=YYYY-MM-DD`.
55
+ - `PUT /api/students/{id}`: Manual override to rename or change a roll number.
56
+ - `DELETE /api/students/{id}`: Purges a student and all historical logs.
57
+ - `POST /api/attendance/manual`: Enables manual toggling of "Present" status without the camera.
58
+
59
+ ## Testing
60
+
61
+ A programmatic integration test suite mimicking camera requests is available:
62
+ ```bash
63
+ python test_core_flows.py
64
+ ```
__pycache__/main.cpython-313.pyc ADDED
Binary file (16.8 kB). View file
 
init_db.py ADDED
@@ -0,0 +1,56 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import psycopg
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+
7
+ DATABASE_URL = os.getenv("DATABASE_URL")
8
+
9
+ if not DATABASE_URL:
10
+ raise ValueError("DATABASE_URL environment variable is not set")
11
+
12
+ print(f"Connecting to database...")
13
+
14
+ def init_db():
15
+ try:
16
+ # Connect to the Neon database
17
+ with psycopg.connect(DATABASE_URL) as conn:
18
+ with conn.cursor() as cur:
19
+ print("Connected! Enabling vector extension...")
20
+ cur.execute("CREATE EXTENSION IF NOT EXISTS vector;")
21
+
22
+ print("Creating students table...")
23
+ cur.execute("""
24
+ CREATE TABLE IF NOT EXISTS students (
25
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
26
+ name VARCHAR(255) NOT NULL,
27
+ roll_number VARCHAR(100) UNIQUE NOT NULL,
28
+ -- Note: We use an arbitrary vector dimension size
29
+ -- However, we must specify the exact dimension if we want pgvector HNSW index.
30
+ -- dlib face embeddings (used by face_recognition) are 128 dimensions.
31
+ -- deepface (VGG-Face) is 2622, Facenet is 128, ArcFace is 512, Datashape is 512.
32
+ -- By default deepface uses VGG-Face (2622) or Facenet (128). Let's use 128 (Facenet or Dlib) or we can leave it dynamic without dimension limit.
33
+ -- Since we switched to deepface, let's just use `vector` (dynamic)
34
+ face_encoding vector NOT NULL,
35
+ created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
36
+ );
37
+ """)
38
+
39
+ print("Creating attendance table...")
40
+ cur.execute("""
41
+ CREATE TABLE IF NOT EXISTS attendance (
42
+ id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
43
+ student_id UUID REFERENCES students(id) ON DELETE CASCADE,
44
+ timestamp TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
45
+ status VARCHAR(50) DEFAULT 'Present'
46
+ );
47
+ """)
48
+
49
+ conn.commit()
50
+ print("Database initialized successfully!")
51
+
52
+ except Exception as e:
53
+ print(f"Error initializing database: {e}")
54
+
55
+ if __name__ == "__main__":
56
+ init_db()
main.py ADDED
@@ -0,0 +1,317 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from fastapi import FastAPI, UploadFile, File, Form, HTTPException
3
+ from fastapi.middleware.cors import CORSMiddleware
4
+ from pydantic import BaseModel
5
+ import psycopg
6
+ from pgvector.psycopg import register_vector
7
+ import numpy as np
8
+ import uuid
9
+ import io
10
+ from PIL import Image
11
+ from deepface import DeepFace
12
+ from datetime import date
13
+ from dotenv import load_dotenv
14
+
15
+ load_dotenv()
16
+
17
+ app = FastAPI(title="VisionAttend API")
18
+
19
+ # Configure CORS
20
+ app.add_middleware(
21
+ CORSMiddleware,
22
+ allow_origins=["*"], # In production, replace with frontend URL
23
+ allow_credentials=True,
24
+ allow_methods=["*"],
25
+ allow_headers=["*"],
26
+ )
27
+
28
+ DATABASE_URL = os.getenv("DATABASE_URL")
29
+
30
+ class MatchResponse(BaseModel):
31
+ message: str
32
+ student_name: str
33
+ roll_number: str
34
+
35
+ from psycopg_pool import ConnectionPool
36
+
37
+ def configure_conn(conn):
38
+ register_vector(conn)
39
+
40
+ # By using a ConnectionPool, we keep the TCP connection alive to Neon.
41
+ # This eliminates the ~6-8 second TLS/SSL handshake delay on every request.
42
+ db_pool = ConnectionPool(
43
+ conninfo=DATABASE_URL,
44
+ configure=configure_conn,
45
+ min_size=1,
46
+ max_size=5,
47
+ timeout=30.0
48
+ )
49
+
50
+ def get_db_connection():
51
+ return db_pool.connection()
52
+
53
+ def extract_face_embedding(file_bytes: bytes) -> tuple[list[float], str]:
54
+ try:
55
+ image = Image.open(io.BytesIO(file_bytes)).convert("RGB")
56
+ img_array = np.array(image)
57
+
58
+ results = DeepFace.represent(
59
+ img_path=img_array,
60
+ enforce_detection=True,
61
+ model_name="Facenet",
62
+ detector_backend="opencv"
63
+ )
64
+
65
+ if len(results) == 0:
66
+ raise ValueError("No face detected.")
67
+ if len(results) > 1:
68
+ raise ValueError("Multiple faces detected. Please show only one face.")
69
+
70
+ embedding = results[0]["embedding"]
71
+ return embedding, "success"
72
+ except ValueError as ve:
73
+ return [], str(ve)
74
+ except Exception as e:
75
+ return [], f"An error occurred during face extraction: {str(e)}"
76
+
77
+ @app.post("/api/register")
78
+ async def register_student(
79
+ name: str = Form(...),
80
+ roll_number: str = Form(...),
81
+ image: UploadFile = File(...)
82
+ ):
83
+ file_bytes = await image.read()
84
+
85
+ embedding, error = extract_face_embedding(file_bytes)
86
+ if not embedding:
87
+ raise HTTPException(status_code=400, detail=error)
88
+
89
+ try:
90
+ with get_db_connection() as conn:
91
+ with conn.cursor() as cur:
92
+ cur.execute("SELECT id FROM students WHERE roll_number = %s", (roll_number,))
93
+ if cur.fetchone():
94
+ raise HTTPException(status_code=400, detail="Student with this roll number already exists.")
95
+
96
+ cur.execute(
97
+ "INSERT INTO students (name, roll_number, face_encoding) VALUES (%s, %s, %s) RETURNING id",
98
+ (name, roll_number, str(embedding))
99
+ )
100
+ student_id = cur.fetchone()[0]
101
+ conn.commit()
102
+
103
+ return {"message": "Student registered successfully", "student_id": student_id}
104
+ except HTTPException:
105
+ raise
106
+ except Exception as e:
107
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
108
+
109
+
110
+ @app.post("/api/recognize", response_model=MatchResponse)
111
+ async def recognize_student(image: UploadFile = File(...)):
112
+ file_bytes = await image.read()
113
+
114
+ embedding, error = extract_face_embedding(file_bytes)
115
+ if not embedding:
116
+ raise HTTPException(status_code=400, detail=error)
117
+
118
+ try:
119
+ with get_db_connection() as conn:
120
+ with conn.cursor() as cur:
121
+ cur.execute("""
122
+ SELECT id, name, roll_number, (face_encoding <=> %s::vector) AS distance
123
+ FROM students
124
+ ORDER BY distance ASC
125
+ LIMIT 1;
126
+ """, (str(embedding),))
127
+
128
+ result = cur.fetchone()
129
+
130
+ if not result:
131
+ raise HTTPException(status_code=404, detail="No match found in database.")
132
+
133
+ student_id, name, roll_number, distance = result
134
+ print(f"Discovered {name} with distance {distance}")
135
+
136
+ if distance > 0.40: # Threshold for Facenet
137
+ raise HTTPException(status_code=404, detail="Face recognized but distance exceeds confidence threshold.")
138
+
139
+ cur.execute("""
140
+ SELECT id FROM attendance
141
+ WHERE student_id = %s AND timestamp::date = %s
142
+ """, (student_id, date.today()))
143
+
144
+ if not cur.fetchone():
145
+ cur.execute(
146
+ "INSERT INTO attendance (student_id, status) VALUES (%s, 'Present')",
147
+ (student_id,)
148
+ )
149
+ conn.commit()
150
+ message = "Attendance marked."
151
+ else:
152
+ message = "Attendance already marked for today."
153
+
154
+ return MatchResponse(message=message, student_name=name, roll_number=roll_number)
155
+
156
+ except HTTPException:
157
+ raise
158
+ except Exception as e:
159
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
160
+
161
+ @app.get("/api/attendance")
162
+ async def get_attendance(date_filter: str = None):
163
+ try:
164
+ with get_db_connection() as conn:
165
+ with conn.cursor() as cur:
166
+ if date_filter:
167
+ cur.execute("""
168
+ SELECT s.id, s.name, s.roll_number, a.timestamp,
169
+ CASE WHEN a.id IS NOT NULL THEN 'Present' ELSE 'Absent' END as status,
170
+ s.id as student_id
171
+ FROM students s
172
+ LEFT JOIN attendance a ON s.id = a.student_id AND DATE(a.timestamp) = %s
173
+ ORDER BY
174
+ CASE WHEN a.id IS NOT NULL THEN 0 ELSE 1 END,
175
+ s.name ASC
176
+ LIMIT 100;
177
+ """, (date_filter,))
178
+ rows = cur.fetchall()
179
+ logs = [
180
+ {
181
+ "id": str(row[0]),
182
+ "name": row[1],
183
+ "roll_number": row[2],
184
+ "timestamp": row[3],
185
+ "status": row[4],
186
+ "student_id": str(row[5])
187
+ } for row in rows
188
+ ]
189
+ else:
190
+ cur.execute("""
191
+ SELECT a.id, s.name, s.roll_number, a.timestamp, a.status, s.id as student_id
192
+ FROM attendance a
193
+ JOIN students s ON a.student_id = s.id
194
+ ORDER BY a.timestamp DESC
195
+ LIMIT 100;
196
+ """)
197
+ rows = cur.fetchall()
198
+ logs = [
199
+ {
200
+ "id": str(row[0]),
201
+ "name": row[1],
202
+ "roll_number": row[2],
203
+ "timestamp": row[3],
204
+ "status": row[4],
205
+ "student_id": str(row[5])
206
+ } for row in rows
207
+ ]
208
+ return {"logs": logs}
209
+ except Exception as e:
210
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
211
+
212
+ class StudentUpdate(BaseModel):
213
+ name: str
214
+ roll_number: str
215
+
216
+ @app.put("/api/students/{student_id}")
217
+ async def update_student(student_id: uuid.UUID, data: StudentUpdate):
218
+ try:
219
+ with get_db_connection() as conn:
220
+ with conn.cursor() as cur:
221
+ cur.execute("SELECT id FROM students WHERE roll_number = %s AND id != %s", (data.roll_number, student_id))
222
+ if cur.fetchone():
223
+ raise HTTPException(status_code=400, detail="Another student with this roll number already exists.")
224
+
225
+ cur.execute(
226
+ "UPDATE students SET name = %s, roll_number = %s WHERE id = %s RETURNING id",
227
+ (data.name, data.roll_number, student_id)
228
+ )
229
+ if not cur.fetchone():
230
+ raise HTTPException(status_code=404, detail="Student not found.")
231
+ conn.commit()
232
+ return {"message": "Student updated successfully."}
233
+ except HTTPException:
234
+ raise
235
+ except Exception as e:
236
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
237
+
238
+ @app.delete("/api/students/{student_id}")
239
+ async def delete_student(student_id: uuid.UUID):
240
+ try:
241
+ with get_db_connection() as conn:
242
+ with conn.cursor() as cur:
243
+ cur.execute("DELETE FROM students WHERE id = %s RETURNING id", (student_id,))
244
+ if not cur.fetchone():
245
+ raise HTTPException(status_code=404, detail="Student not found.")
246
+ conn.commit()
247
+ return {"message": "Student deleted successfully."}
248
+ except HTTPException:
249
+ raise
250
+ except Exception as e:
251
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
252
+
253
+ @app.put("/api/students/{student_id}/photo")
254
+ async def update_student_photo(student_id: uuid.UUID, image: UploadFile = File(...)):
255
+ file_bytes = await image.read()
256
+ embedding, error = extract_face_embedding(file_bytes)
257
+ if not embedding:
258
+ raise HTTPException(status_code=400, detail=error)
259
+
260
+ try:
261
+ with get_db_connection() as conn:
262
+ with conn.cursor() as cur:
263
+ cur.execute(
264
+ "UPDATE students SET face_encoding = %s WHERE id = %s RETURNING id",
265
+ (str(embedding), student_id)
266
+ )
267
+ if not cur.fetchone():
268
+ raise HTTPException(status_code=404, detail="Student not found.")
269
+ conn.commit()
270
+ return {"message": "Student photo updated successfully."}
271
+ except HTTPException:
272
+ raise
273
+ except Exception as e:
274
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
275
+
276
+ class ManualAttendance(BaseModel):
277
+ student_id: uuid.UUID
278
+ date: str # YYYY-MM-DD
279
+ status: str # "Present" or "Absent"
280
+
281
+ @app.post("/api/attendance/manual")
282
+ async def mark_manual_attendance(data: ManualAttendance):
283
+ try:
284
+ with get_db_connection() as conn:
285
+ with conn.cursor() as cur:
286
+ if data.status == "Present":
287
+ cur.execute("""
288
+ SELECT id FROM attendance
289
+ WHERE student_id = %s AND timestamp::date = %s
290
+ """, (data.student_id, data.date))
291
+
292
+ if not cur.fetchone():
293
+ cur.execute(
294
+ "INSERT INTO attendance (student_id, status, timestamp) VALUES (%s, 'Present', %s)",
295
+ (data.student_id, f"{data.date} 12:00:00") # Default to noon for manual entries
296
+ )
297
+ conn.commit()
298
+ return {"message": "Marked present successfully."}
299
+ else:
300
+ return {"message": "Already marked present for this date."}
301
+ elif data.status == "Absent":
302
+ cur.execute("""
303
+ DELETE FROM attendance
304
+ WHERE student_id = %s AND timestamp::date = %s
305
+ """, (data.student_id, data.date))
306
+ conn.commit()
307
+ return {"message": "Marked absent successfully."}
308
+ else:
309
+ raise HTTPException(status_code=400, detail="Invalid status.")
310
+ except HTTPException:
311
+ raise
312
+ except Exception as e:
313
+ raise HTTPException(status_code=500, detail=f"Database error: {str(e)}")
314
+
315
+ @app.get("/")
316
+ def read_root():
317
+ return {"status": "ok", "message": "VisionAttend API is active"}
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ psycopg[binary]
4
+ pgvector
5
+ python-multipart
6
+ numpy
7
+ deepface
8
+ tf-keras
9
+ pydantic
10
+ python-dotenv
11
+ cors
test_core_flows.py ADDED
@@ -0,0 +1,132 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import os
3
+ import sys
4
+ import uuid
5
+ from datetime import date
6
+
7
+ # Base URL for the FastAPI server
8
+ BASE_URL = "http://localhost:8000"
9
+
10
+ # Path to the test image
11
+ IMAGE_PATH = "test_face.jpg"
12
+
13
+ def download_test_image():
14
+ if not os.path.exists(IMAGE_PATH):
15
+ print(f"Downloading test image to {IMAGE_PATH}...")
16
+ url = "https://raw.githubusercontent.com/opencv/opencv/master/samples/data/lena.jpg"
17
+ response = requests.get(url)
18
+ if response.status_code == 200:
19
+ with open(IMAGE_PATH, 'wb') as f:
20
+ f.write(response.content)
21
+ print("Downloaded successfully.")
22
+ else:
23
+ print("Failed to download test image.")
24
+ sys.exit(1)
25
+
26
+ def run_tests():
27
+ print("--- VisionAttend Integration Tests ---")
28
+
29
+ # Generate unique roll number to avoid constraint errors
30
+ roll_number = f"TEST-{uuid.uuid4().hex[:6].upper()}"
31
+ student_name = "Test Student"
32
+ student_id = None
33
+
34
+ print("\n[1] Testing Registration...")
35
+ try:
36
+ with open(IMAGE_PATH, 'rb') as f:
37
+ files = {'image': (IMAGE_PATH, f, 'image/jpeg')}
38
+ data = {'name': student_name, 'roll_number': roll_number}
39
+ response = requests.post(f"{BASE_URL}/api/register", data=data, files=files)
40
+
41
+ if response.status_code == 200:
42
+ result = response.json()
43
+ student_id = result.get('student_id')
44
+ print(f"βœ… Registration successful. Student ID: {student_id}")
45
+ else:
46
+ print(f"❌ Registration failed: {response.text}")
47
+ return
48
+ except Exception as e:
49
+ print(f"❌ Registration exception: {e}")
50
+ return
51
+
52
+ print("\n[2] Testing Recognition & Attendance Marking...")
53
+ try:
54
+ with open(IMAGE_PATH, 'rb') as f:
55
+ files = {'image': (IMAGE_PATH, f, 'image/jpeg')}
56
+ response = requests.post(f"{BASE_URL}/api/recognize", files=files)
57
+
58
+ if response.status_code == 200:
59
+ result = response.json()
60
+ print(f"βœ… Recognition successful: {result['message']} - {result['student_name']} ({result['roll_number']})")
61
+ assert result['roll_number'] == roll_number
62
+ else:
63
+ print(f"❌ Recognition failed: {response.text}")
64
+ except Exception as e:
65
+ print(f"❌ Recognition exception: {e}")
66
+
67
+ print("\n[3] Testing Attendance Logs Query...")
68
+ try:
69
+ today_str = date.today().strftime("%Y-%m-%d")
70
+ response = requests.get(f"{BASE_URL}/api/attendance?date_filter={today_str}")
71
+ if response.status_code == 200:
72
+ logs = response.json().get('logs', [])
73
+ found = False
74
+ for log in logs:
75
+ if log.get('student_id') == str(student_id):
76
+ found = True
77
+ print(f"βœ… Found student in logs. Status: {log.get('status')}")
78
+ assert log.get('status') == 'Present'
79
+ break
80
+ if not found:
81
+ print("❌ Student not found in attendance logs.")
82
+ else:
83
+ print(f"❌ Attendance query failed: {response.text}")
84
+ except Exception as e:
85
+ print(f"❌ Attendance query exception: {e}")
86
+
87
+ print("\n[4] Testing Manual Attendance Update...")
88
+ try:
89
+ data = {
90
+ "student_id": str(student_id),
91
+ "date": date.today().strftime("%Y-%m-%d"),
92
+ "status": "Absent"
93
+ }
94
+ response = requests.post(f"{BASE_URL}/api/attendance/manual", json=data)
95
+ if response.status_code == 200:
96
+ print("βœ… Manual attendance marked successfully.")
97
+ else:
98
+ print(f"❌ Manual attendance failed: {response.text}")
99
+ except Exception as e:
100
+ print(f"❌ Manual attendance exception: {e}")
101
+
102
+ print("\n[5] Testing Student Details Update...")
103
+ try:
104
+ new_name = "Updated Test Student"
105
+ # We'll use the same roll_number as checking existing logic
106
+ data = {
107
+ "name": new_name,
108
+ "roll_number": roll_number
109
+ }
110
+ response = requests.put(f"{BASE_URL}/api/students/{student_id}", json=data)
111
+ if response.status_code == 200:
112
+ print("βœ… Student details updated successfully.")
113
+ else:
114
+ print(f"❌ Student detail update failed: {response.text}")
115
+ except Exception as e:
116
+ print(f"❌ Student detail update exception: {e}")
117
+
118
+ print("\n[6] Testing Student Deletion (Cleanup)...")
119
+ if student_id:
120
+ try:
121
+ response = requests.delete(f"{BASE_URL}/api/students/{student_id}")
122
+ if response.status_code == 200:
123
+ print("βœ… Student deleted successfully.")
124
+ else:
125
+ print(f"❌ Student deletion failed: {response.text}")
126
+ except Exception as e:
127
+ print(f"❌ Student deletion exception: {e}")
128
+
129
+
130
+ if __name__ == "__main__":
131
+ download_test_image()
132
+ run_tests()
test_db.py ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import psycopg
2
+ import os
3
+ from dotenv import load_dotenv
4
+
5
+ load_dotenv()
6
+ db_url = os.environ.get("DATABASE_URL")
7
+
8
+ conn = psycopg.connect(db_url)
9
+ print("Connected.", conn.closed)
10
+
11
+ with conn:
12
+ print("Inside with block")
13
+
14
+ print("After with block, closed:", conn.closed)
test_face.jpg ADDED