Testys commited on
Commit
a09ee49
·
1 Parent(s): 6a954ce

WIP: Making sure the clearance system can run live with Fastapi

Browse files
Files changed (8) hide show
  1. .gitignore +174 -0
  2. README copy.md +1 -0
  3. main.py +191 -0
  4. requirements.txt +11 -0
  5. src/auth.py +30 -0
  6. src/crud.py +151 -0
  7. src/database.py +100 -0
  8. src/models.py +52 -0
.gitignore ADDED
@@ -0,0 +1,174 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Byte-compiled / optimized / DLL files
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+
6
+ # C extensions
7
+ *.so
8
+
9
+ # Distribution / packaging
10
+ .Python
11
+ build/
12
+ develop-eggs/
13
+ dist/
14
+ downloads/
15
+ eggs/
16
+ .eggs/
17
+ lib/
18
+ lib64/
19
+ parts/
20
+ sdist/
21
+ var/
22
+ wheels/
23
+ share/python-wheels/
24
+ *.egg-info/
25
+ .installed.cfg
26
+ *.egg
27
+ MANIFEST
28
+
29
+ # PyInstaller
30
+ # Usually these files are written by a python script from a template
31
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
32
+ *.manifest
33
+ *.spec
34
+
35
+ # Installer logs
36
+ pip-log.txt
37
+ pip-delete-this-directory.txt
38
+
39
+ # Unit test / coverage reports
40
+ htmlcov/
41
+ .tox/
42
+ .nox/
43
+ .coverage
44
+ .coverage.*
45
+ .cache
46
+ nosetests.xml
47
+ coverage.xml
48
+ *.cover
49
+ *.py,cover
50
+ .hypothesis/
51
+ .pytest_cache/
52
+ cover/
53
+
54
+ # Translations
55
+ *.mo
56
+ *.pot
57
+
58
+ # Django stuff:
59
+ *.log
60
+ local_settings.py
61
+ db.sqlite3
62
+ db.sqlite3-journal
63
+
64
+ # Flask stuff:
65
+ instance/
66
+ .webassets-cache
67
+
68
+ # Scrapy stuff:
69
+ .scrapy
70
+
71
+ # Sphinx documentation
72
+ docs/_build/
73
+
74
+ # PyBuilder
75
+ .pybuilder/
76
+ target/
77
+
78
+ # Jupyter Notebook
79
+ .ipynb_checkpoints
80
+
81
+ # IPython
82
+ profile_default/
83
+ ipython_config.py
84
+
85
+ # pyenv
86
+ # For a library or package, you might want to ignore these files since the code is
87
+ # intended to run in multiple environments; otherwise, check them in:
88
+ # .python-version
89
+
90
+ # pipenv
91
+ # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
92
+ # However, in case of collaboration, if having platform-specific dependencies or dependencies
93
+ # having no cross-platform support, pipenv may install dependencies that don't work, or not
94
+ # install all needed dependencies.
95
+ #Pipfile.lock
96
+
97
+ # UV
98
+ # Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
99
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
100
+ # commonly ignored for libraries.
101
+ #uv.lock
102
+
103
+ # poetry
104
+ # Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
105
+ # This is especially recommended for binary packages to ensure reproducibility, and is more
106
+ # commonly ignored for libraries.
107
+ # https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
108
+ #poetry.lock
109
+
110
+ # pdm
111
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
112
+ #pdm.lock
113
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
114
+ # in version control.
115
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
116
+ .pdm.toml
117
+ .pdm-python
118
+ .pdm-build/
119
+
120
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
121
+ __pypackages__/
122
+
123
+ # Celery stuff
124
+ celerybeat-schedule
125
+ celerybeat.pid
126
+
127
+ # SageMath parsed files
128
+ *.sage.py
129
+
130
+ # Environments
131
+ .env
132
+ .venv
133
+ env/
134
+ venv/
135
+ ENV/
136
+ env.bak/
137
+ venv.bak/
138
+
139
+ # Spyder project settings
140
+ .spyderproject
141
+ .spyproject
142
+
143
+ # Rope project settings
144
+ .ropeproject
145
+
146
+ # mkdocs documentation
147
+ /site
148
+
149
+ # mypy
150
+ .mypy_cache/
151
+ .dmypy.json
152
+ dmypy.json
153
+
154
+ # Pyre type checker
155
+ .pyre/
156
+
157
+ # pytype static type analyzer
158
+ .pytype/
159
+
160
+ # Cython debug symbols
161
+ cython_debug/
162
+
163
+ # PyCharm
164
+ # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
165
+ # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
166
+ # and can be added to the global gitignore or merged into this file. For a more nuclear
167
+ # option (not recommended) you can uncomment the following to ignore the entire idea folder.
168
+ #.idea/
169
+
170
+ # Ruff stuff:
171
+ .ruff_cache/
172
+
173
+ # PyPI configuration file
174
+ .pypirc
README copy.md ADDED
@@ -0,0 +1 @@
 
 
1
+ # clearance_stud
main.py ADDED
@@ -0,0 +1,191 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, HTTPException, Depends, status
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ import uvicorn
4
+ from typing import List # Import List for type hinting
5
+ from src.database import connect_db, disconnect_db, database # Also import database instance if needed directly in main
6
+ from src.models import (
7
+ StudentCreate, Student, ClearanceStatusCreate,
8
+ ClearanceStatus, ClearanceDetail, DeviceRegister,
9
+ DeviceResponse, TagScan
10
+ ) # Pydantic models
11
+ from src import crud # Database operations (now uses databases)
12
+ from src.auth import verify_api_key # API key verification dependency (now uses databases)
13
+
14
+ # FastAPI app instance
15
+ app = FastAPI(title="Undergraduate Clearance System API", version="1.0.0")
16
+
17
+ # Enable CORS (Cross-Origin Resource Sharing)
18
+ # This allows your frontend (running on a different origin) to access the API
19
+ app.add_middleware(
20
+ CORSMiddleware,
21
+ allow_origins=["*"], # WARNING: Use specific origins in production (e.g., ["http://localhost:3000", "https://your-frontend-domain.com"])
22
+ allow_credentials=True,
23
+ allow_methods=["*"], # Allow all HTTP methods (GET, POST, PUT, DELETE, etc.)
24
+ allow_headers=["*"], # Allow all headers
25
+ )
26
+
27
+ # --- Database Connection Lifecycle ---
28
+
29
+ # Connect to the database when the application starts up
30
+ @app.on_event("startup")
31
+ async def startup_event():
32
+ await connect_db()
33
+
34
+ # Disconnect from the database when the application shuts down
35
+ @app.on_event("shutdown")
36
+ async def shutdown_event():
37
+ await disconnect_db()
38
+
39
+
40
+ # --- Routes for ESP32 Devices ---
41
+
42
+ @app.post("/api/devices/register", response_model=DeviceResponse, summary="Register or re-register an ESP32 device")
43
+ async def register_device(device: DeviceRegister):
44
+ """
45
+ Registers a new ESP32 device or updates an existing one using 'databases'.
46
+ Returns a unique API key for the device to use for subsequent requests.
47
+ """
48
+ # Call the crud function, which now uses 'databases'
49
+ return await crud.register_device(device)
50
+
51
+ @app.post("/api/scan", summary="Receive tag scan data from an ESP32 device and return clearance details")
52
+ async def scan_tag(scan_data: TagScan):
53
+ """
54
+ Receives a tag ID from an ESP32 device, verifies the device's API key
55
+ using 'databases', logs the scan event, retrieves the student's
56
+ clearance details based on the tag ID, and returns the details to the device.
57
+ """
58
+ # Verify API key using the dependency - this will raise HTTPException if invalid
59
+ # The verify_api_key function now uses 'databases'
60
+ device = await verify_api_key(scan_data.api_key)
61
+
62
+ # Update device last seen timestamp using the crud function (which uses databases)
63
+ await crud.update_device_last_seen(scan_data.device_id)
64
+
65
+ # Log the scan event using the crud function (which uses databases)
66
+ await crud.create_device_log(scan_data.device_id, scan_data.tag_id, "scan")
67
+
68
+ # Get student information by tag ID using the crud function (which uses databases)
69
+ student = await crud.get_student_by_tag_id(scan_data.tag_id)
70
+
71
+ if not student:
72
+ # Return a specific message to the device if student is not found
73
+ # The ESP32 firmware should handle this 404 response
74
+ raise HTTPException(status_code=404, detail="Student not found for this tag")
75
+
76
+ # Get clearance statuses for the student using the crud function (which uses databases)
77
+ statuses = await crud.get_clearance_statuses_by_student_id(student["student_id"])
78
+
79
+ # Format clearance items and determine overall status
80
+ clearance_items = []
81
+ overall_status = True
82
+
83
+ for status_item in statuses:
84
+ clearance_items.append({
85
+ "department": status_item["department"],
86
+ "status": status_item["status"],
87
+ "remarks": status_item["remarks"],
88
+ "updated_at": status_item["updated_at"].isoformat() # Format datetime to ISO string
89
+ })
90
+ if not status_item["status"]:
91
+ overall_status = False
92
+
93
+ # Return the formatted clearance details
94
+ return {
95
+ "student_id": student["student_id"],
96
+ "name": student["name"],
97
+ "department": student["department"],
98
+ "clearance_items": clearance_items,
99
+ "overall_status": overall_status
100
+ }
101
+
102
+ # --- Student Management Routes (Likely for Frontend/Admin Use) ---
103
+
104
+ @app.post("/api/students/", response_model=Student, status_code=status.HTTP_201_CREATED, summary="Create a new student")
105
+ async def create_student(student: StudentCreate):
106
+ """Creates a new student record using 'databases'."""
107
+ # Check if student ID already exists using the crud function (which uses databases)
108
+ existing_student_by_id = await crud.get_student_by_student_id(student.student_id)
109
+ if existing_student_by_id:
110
+ raise HTTPException(status_code=400, detail="Student ID already registered")
111
+
112
+ # Check if tag ID already exists using the crud function (which uses databases)
113
+ existing_student_by_tag = await crud.get_student_by_tag_id(student.tag_id)
114
+ if existing_student_by_tag:
115
+ raise HTTPException(status_code=400, detail="Tag ID already assigned to another student")
116
+
117
+ # Create the student using the crud function (which uses databases)
118
+ return await crud.create_student(student)
119
+
120
+ @app.get("/api/students/", response_model=List[Student], summary="Get all students")
121
+ async def get_students():
122
+ """Retrieves a list of all students using 'databases'."""
123
+ # Get all students using the crud function (which uses databases)
124
+ return await crud.get_all_students()
125
+
126
+ @app.get("/api/students/{student_id}", response_model=ClearanceDetail, summary="Get clearance details for a specific student")
127
+ async def get_student_clearance(student_id: str):
128
+ """
129
+ Retrieves the full clearance details for a student based on their student ID
130
+ using 'databases'. This endpoint is likely used by the frontend/admin interface.
131
+ """
132
+ # Get student info using the crud function (which uses databases)
133
+ student = await crud.get_student_by_student_id(student_id)
134
+
135
+ if not student:
136
+ raise HTTPException(status_code=404, detail="Student not found")
137
+
138
+ # Get clearance status using the crud function (which uses databases)
139
+ statuses = await crud.get_clearance_statuses_by_student_id(student_id)
140
+
141
+ # Format clearance items and determine overall status
142
+ clearance_items = []
143
+ overall_status = True
144
+
145
+ for status_item in statuses:
146
+ clearance_items.append({
147
+ "department": status_item["department"],
148
+ "status": status_item["status"],
149
+ "remarks": status_item["remarks"],
150
+ "updated_at": status_item["updated_at"].isoformat() # Format datetime to ISO string
151
+ })
152
+ if not status_item["status"]:
153
+ overall_status = False
154
+
155
+ # Return the formatted clearance details
156
+ return {
157
+ "student_id": student["student_id"],
158
+ "name": student["name"],
159
+ "department": student["department"],
160
+ "clearance_items": clearance_items,
161
+ "overall_status": overall_status
162
+ }
163
+
164
+ # --- Clearance Management Routes (Likely for Frontend/Admin Use) ---
165
+
166
+ @app.post("/api/clearance/", response_model=ClearanceStatus, summary="Create or update a student's clearance status for a department")
167
+ async def update_clearance_status(status_data: ClearanceStatusCreate):
168
+ """
169
+ Creates a new clearance status entry for a student and department,
170
+ or updates an existing one using 'databases'.
171
+ """
172
+ # Check if student exists using the crud function (which uses databases)
173
+ student = await crud.get_student_by_student_id(status_data.student_id)
174
+ if not student:
175
+ raise HTTPException(status_code=404, detail="Student not found")
176
+
177
+ # Create or update the clearance status using the crud function (which uses databases)
178
+ return await crud.create_or_update_clearance_status(status_data)
179
+
180
+
181
+ # --- Root Endpoint (Optional) ---
182
+ @app.get("/", summary="Root endpoint")
183
+ async def read_root():
184
+ """Basic root endpoint to confirm the API is running."""
185
+ return {"message": "Undergraduate Clearance System API is running"}
186
+
187
+
188
+ # --- Run the FastAPI app ---
189
+ if __name__ == "__main__":
190
+ # Use uvicorn to run the FastAPI application
191
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)
requirements.txt ADDED
@@ -0,0 +1,11 @@
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi
2
+ uvicorn
3
+ databases[postgresql]
4
+ asyncpg
5
+ pydantic
6
+ pydantic[email]
7
+ pydantic[email,cryptography]
8
+ python-dotenv
9
+ sqlalchemy
10
+ supabase
11
+ psycopg2-binary
src/auth.py ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException, status, Depends
2
+ from src.database import database, devices # Import database instance and devices table
3
+
4
+ # Dependency to verify the API key sent by ESP32 devices
5
+ async def verify_api_key(api_key: str = Depends(HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Missing API key"))):
6
+ """
7
+ Verifies if the provided API key is valid and corresponds to a registered device
8
+ using the 'databases' library.
9
+
10
+ Args:
11
+ api_key: The API key provided in the request header or body.
12
+
13
+ Returns:
14
+ The database record of the device if the API key is valid.
15
+
16
+ Raises:
17
+ HTTPException: If the API key is invalid or not found.
18
+ """
19
+ # Use 'databases' to execute a select query on the 'devices' table
20
+ query = devices.select().where(devices.c.api_key == api_key)
21
+ device = await database.fetch_one(query) # fetch_one returns a single record or None
22
+
23
+ if not device:
24
+ # Raise HTTPException if the API key is not found
25
+ raise HTTPException(
26
+ status_code=status.HTTP_401_UNAUTHORIZED,
27
+ detail="Invalid API key",
28
+ headers={"WWW-Authenticate": "Bearer"}, # Optional: Suggest Bearer token scheme
29
+ )
30
+ return device
src/crud.py ADDED
@@ -0,0 +1,151 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Import database instance and tables from database.py
2
+ from src.database import database, students, clearance_statuses, device_logs, devices
3
+ from src.models import StudentCreate, ClearanceStatusCreate, TagScan, DeviceRegister # Import Pydantic models
4
+ # Import necessary SQLAlchemy components for building queries
5
+ from sqlalchemy import select, insert, update
6
+ from datetime import datetime
7
+ import secrets # For generating API keys
8
+ from fastapi import HTTPException # Import HTTPException to raise errors
9
+
10
+ # --- CRUD operations for Students ---
11
+
12
+ async def create_student(student: StudentCreate):
13
+ """Creates a new student record in the database using 'databases'."""
14
+ # Build an insert query using SQLAlchemy
15
+ query = students.insert().values(
16
+ student_id=student.student_id,
17
+ name=student.name,
18
+ department=student.department,
19
+ tag_id=student.tag_id
20
+ )
21
+ # Execute the query asynchronously using 'databases'
22
+ last_record_id = await database.execute(query)
23
+ # Return the created student data including the generated ID
24
+ return {**student.dict(), "id": last_record_id}
25
+
26
+ async def get_all_students():
27
+ """Retrieves all student records from the database using 'databases'."""
28
+ # Build a select query using SQLAlchemy
29
+ query = students.select()
30
+ # Execute the query and fetch all results asynchronously
31
+ return await database.fetch_all(query)
32
+
33
+ async def get_student_by_student_id(student_id: str):
34
+ """Retrieves a student record by their student ID using 'databases'."""
35
+ # Build a select query with a where clause
36
+ query = students.select().where(students.c.student_id == student_id)
37
+ # Execute the query and fetch one result asynchronously
38
+ return await database.fetch_one(query)
39
+
40
+ async def get_student_by_tag_id(tag_id: str):
41
+ """Retrieves a student record by their tag ID using 'databases'."""
42
+ # Build a select query with a where clause
43
+ query = students.select().where(students.c.tag_id == tag_id)
44
+ # Execute the query and fetch one result asynchronously
45
+ return await database.fetch_one(query)
46
+
47
+ # --- CRUD operations for Clearance Statuses ---
48
+
49
+ async def create_or_update_clearance_status(status_data: ClearanceStatusCreate):
50
+ """Creates a new clearance status or updates an existing one using 'databases'."""
51
+ # Check if clearance status already exists for this student and department
52
+ query = select(clearance_statuses).where(
53
+ (clearance_statuses.c.student_id == status_data.student_id) &
54
+ (clearance_statuses.c.department == status_data.department)
55
+ )
56
+ existing_status = await database.fetch_one(query)
57
+
58
+ current_time = datetime.utcnow()
59
+
60
+ if existing_status:
61
+ # Update existing status
62
+ query = update(clearance_statuses).where(
63
+ (clearance_statuses.c.student_id == status_data.student_id) &
64
+ (clearance_statuses.c.department == status_data.department)
65
+ ).values(
66
+ status=status_data.status,
67
+ remarks=status_data.remarks,
68
+ updated_at=current_time
69
+ )
70
+ await database.execute(query)
71
+ # Return the updated record including the existing ID and new timestamp
72
+ return {
73
+ **status_data.dict(),
74
+ "id": existing_status["id"],
75
+ "updated_at": current_time
76
+ }
77
+ else:
78
+ # Create new status
79
+ query = insert(clearance_statuses).values(
80
+ student_id=status_data.student_id,
81
+ department=status_data.department,
82
+ status=status_data.status,
83
+ remarks=status_data.remarks,
84
+ updated_at=current_time
85
+ )
86
+ last_record_id = await database.execute(query)
87
+ # Return the newly created record including the new ID and timestamp
88
+ return {
89
+ **status_data.dict(),
90
+ "id": last_record_id,
91
+ "updated_at": current_time
92
+ }
93
+
94
+
95
+ async def get_clearance_statuses_by_student_id(student_id: str):
96
+ """Retrieves all clearance statuses for a given student ID using 'databases'."""
97
+ query = select(clearance_statuses).where(clearance_statuses.c.student_id == student_id)
98
+ return await database.fetch_all(query)
99
+
100
+ # --- CRUD operations for Devices ---
101
+
102
+ async def register_device(device_data: DeviceRegister):
103
+ """Registers a new device or updates an existing one, generating an API key using 'databases'."""
104
+ api_key = secrets.token_hex(16) # Generate a unique API key
105
+ current_time = datetime.utcnow()
106
+
107
+ # Check if device already exists
108
+ query = select(devices).where(devices.c.device_id == device_data.device_id)
109
+ existing_device = await database.fetch_one(query)
110
+
111
+ if existing_device:
112
+ # Update existing device
113
+ query = update(devices).where(devices.c.device_id == device_data.device_id).values(
114
+ location=device_data.location,
115
+ api_key=api_key, # Assign a new API key on re-registration
116
+ last_seen=current_time
117
+ )
118
+ await database.execute(query)
119
+ else:
120
+ # Create new device
121
+ query = insert(devices).values(
122
+ device_id=device_data.device_id,
123
+ location=device_data.location,
124
+ api_key=api_key,
125
+ last_seen=current_time
126
+ )
127
+ await database.execute(query)
128
+
129
+ # Return the device details including the generated API key
130
+ # We need to fetch the newly created/updated device to get the assigned API key
131
+ query = select(devices).where(devices.c.device_id == device_data.device_id)
132
+ updated_device = await database.fetch_one(query)
133
+ return {"device_id": updated_device["device_id"], "location": updated_device["location"], "api_key": updated_device["api_key"]}
134
+
135
+
136
+ async def update_device_last_seen(device_id: str):
137
+ """Updates the 'last_seen' timestamp for a given device using 'databases'."""
138
+ query = update(devices).where(devices.c.device_id == device_id).values(last_seen=datetime.utcnow())
139
+ await database.execute(query)
140
+
141
+ # --- CRUD operations for Device Logs ---
142
+
143
+ async def create_device_log(device_id: str, tag_id: str, action: str):
144
+ """Creates a log entry for device activity using 'databases'."""
145
+ query = device_logs.insert().values(
146
+ device_id=device_id,
147
+ tag_id=tag_id,
148
+ timestamp=datetime.utcnow(),
149
+ action=action
150
+ )
151
+ await database.execute(query)
src/database.py ADDED
@@ -0,0 +1,100 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import databases
2
+ import sqlalchemy
3
+ import os
4
+ from dotenv import load_dotenv
5
+ from datetime import datetime
6
+
7
+ # Load environment variables from a .env file
8
+ load_dotenv()
9
+
10
+ # Database setup
11
+ # Get the database URL from environment variables
12
+ # IMPORTANT: Ensure your .env file has the correct DATABASE_URL for PostgreSQL
13
+ # Example: DATABASE_URL="postgresql+asyncpg://user:password@host:port/database_name"
14
+ DATABASE_URL = os.getenv("POSTGRES_URI") or os.getenv("DATABASE_URL")
15
+
16
+ # Check if DATABASE_URL is set
17
+ if not DATABASE_URL:
18
+ raise ValueError("DATABASE_URL environment variable not set!")
19
+
20
+ # Create a Database instance for asynchronous operations
21
+ # The 'databases' library uses the connection string to determine the dialect and driver
22
+ database = databases.Database(DATABASE_URL)
23
+
24
+ # Create a MetaData object to hold the database schema
25
+ metadata = sqlalchemy.MetaData()
26
+
27
+ # Define the 'students' table using SQLAlchemy
28
+ students = sqlalchemy.Table(
29
+ "students",
30
+ metadata,
31
+ sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
32
+ sqlalchemy.Column("student_id", sqlalchemy.String, unique=True, index=True), # Added index for faster lookups
33
+ sqlalchemy.Column("name", sqlalchemy.String),
34
+ sqlalchemy.Column("department", sqlalchemy.String),
35
+ sqlalchemy.Column("tag_id", sqlalchemy.String, unique=True, index=True), # Added index
36
+ )
37
+
38
+ # Define the 'clearance_statuses' table using SQLAlchemy
39
+ clearance_statuses = sqlalchemy.Table(
40
+ "clearance_statuses",
41
+ metadata,
42
+ sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
43
+ sqlalchemy.Column("student_id", sqlalchemy.String, index=True), # Added index
44
+ sqlalchemy.Column("department", sqlalchemy.String, index=True), # Added index
45
+ sqlalchemy.Column("status", sqlalchemy.Boolean, default=False),
46
+ sqlalchemy.Column("remarks", sqlalchemy.String, nullable=True),
47
+ sqlalchemy.Column("updated_at", sqlalchemy.DateTime, default=datetime.utcnow),
48
+ # Add a unique constraint to prevent duplicate entries for the same student and department
49
+ sqlalchemy.UniqueConstraint('student_id', 'department', name='uq_student_department')
50
+ )
51
+
52
+ # Define the 'device_logs' table to log device activity using SQLAlchemy
53
+ device_logs = sqlalchemy.Table(
54
+ "device_logs",
55
+ metadata,
56
+ sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
57
+ sqlalchemy.Column("device_id", sqlalchemy.String, index=True), # Added index
58
+ sqlalchemy.Column("tag_id", sqlalchemy.String, index=True), # Added index
59
+ sqlalchemy.Column("timestamp", sqlalchemy.DateTime, default=datetime.utcnow),
60
+ sqlalchemy.Column("action", sqlalchemy.String), # e.g., "scan", "register"
61
+ )
62
+
63
+ # Define the 'devices' table to manage registered ESP32 devices using SQLAlchemy (Location Removed)
64
+ devices = sqlalchemy.Table(
65
+ "devices",
66
+ metadata,
67
+ sqlalchemy.Column("id", sqlalchemy.Integer, primary_key=True),
68
+ sqlalchemy.Column("device_id", sqlalchemy.String, unique=True, index=True), # Added index
69
+ # Removed: sqlalchemy.Column("location", sqlalchemy.String),
70
+ sqlalchemy.Column("api_key", sqlalchemy.String, unique=True, index=True), # Added index for quick API key lookups
71
+ sqlalchemy.Column("last_seen", sqlalchemy.DateTime, nullable=True),
72
+ )
73
+
74
+ # Create a SQLAlchemy engine (used for creating tables - typically run once)
75
+ # Note: For PostgreSQL, check_same_thread=False is not needed.
76
+ # This engine is primarily used here for the metadata.create_all call.
77
+ engine = sqlalchemy.create_engine(DATABASE_URL)
78
+
79
+ try:
80
+ print("Attempting to create database tables (if they don't exist)...")
81
+ metadata.create_all(engine)
82
+ print("Database tables creation attempt finished.")
83
+ except Exception as e:
84
+ print(f"Error during database table creation: {e}")
85
+ print("Please ensure your Supabase database is accessible and the connection string is correct.")
86
+ print("You might need to manually create tables in Supabase using the provided SQL script.")
87
+
88
+
89
+ # Database connection lifecycle events (to be called by FastAPI)
90
+ async def connect_db():
91
+ """Connects to the database on application startup."""
92
+ print("Connecting to database...") # Added print statement for debugging
93
+ await database.connect()
94
+ print("Database connected.") # Added print statement for debugging
95
+
96
+ async def disconnect_db():
97
+ """Disconnects from the database on application shutdown."""
98
+ print("Disconnecting from database...") # Added print statement for debugging
99
+ await database.disconnect()
100
+ print("Database disconnected.") # Added print statement for debugging
src/models.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel
2
+ from typing import List, Optional
3
+ from datetime import datetime
4
+
5
+ # Pydantic models for data validation and serialization
6
+
7
+ class StudentCreate(BaseModel):
8
+ """Model for creating a new student."""
9
+ student_id: str
10
+ name: str
11
+ department: str
12
+ tag_id: str
13
+
14
+ class Student(StudentCreate):
15
+ """Model for returning student data, including database ID."""
16
+ id: int
17
+
18
+ class ClearanceStatusCreate(BaseModel):
19
+ """Model for creating or updating a student's clearance status for a specific department."""
20
+ student_id: str
21
+ department: str
22
+ status: bool
23
+ remarks: Optional[str] = None
24
+
25
+ class ClearanceStatus(ClearanceStatusCreate):
26
+ """Model for returning clearance status data, including database ID and update timestamp."""
27
+ id: int
28
+ updated_at: datetime
29
+
30
+ class ClearanceDetail(BaseModel):
31
+ """Model for returning a student's full clearance details."""
32
+ student_id: str
33
+ name: str
34
+ department: str
35
+ clearance_items: List[dict] # List of dictionaries representing each clearance item
36
+ overall_status: bool
37
+
38
+ class DeviceRegister(BaseModel):
39
+ """Model for registering a new ESP32 device."""
40
+ device_id: str
41
+ location: str
42
+
43
+ class DeviceResponse(DeviceRegister):
44
+ """Model for returning device registration details, including the API key."""
45
+ api_key: str
46
+
47
+ class TagScan(BaseModel):
48
+ """Model for data received from an ESP32 device after scanning a tag."""
49
+ device_id: str
50
+ tag_id: str
51
+ api_key: str
52
+ timestamp: datetime