MuhammedSuhaib commited on
Commit
cdaf146
·
verified ·
1 Parent(s): 014e4c1

Deployment via uv

Browse files
Files changed (17) hide show
  1. .gitignore +47 -0
  2. BACKEND_ARCHITECTURE.md +388 -0
  3. CLAUDE.md +47 -0
  4. Dockerfile +15 -0
  5. README.md +21 -7
  6. auth.db +0 -0
  7. auth/jwt.py +43 -0
  8. database/__init__.py +38 -0
  9. deploy_hf.sh +29 -0
  10. main.py +52 -0
  11. models.py +24 -0
  12. models/user.py +10 -0
  13. requirements.txt +13 -0
  14. routes/tasks.py +127 -0
  15. schemas/tasks.py +39 -0
  16. space.yaml +2 -0
  17. update_neon_schema.sql +17 -0
.gitignore ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ __pycache__/
2
+ *.py[cod]
3
+ *$py.class
4
+ *.so
5
+ .Python
6
+ env/
7
+ venv/
8
+ .venv/
9
+ env.bak/
10
+ venv.bak/
11
+ .env
12
+ *.env
13
+ .env.local
14
+ .env.*.local
15
+ .env.example
16
+ .pytest_cache/
17
+ .coverage
18
+ htmlcov/
19
+ .coverage.*
20
+ .hypothesis/
21
+ .pyre/
22
+ .pytype/
23
+ celerybeat-schedule
24
+ celerybeat.pid
25
+ *.sage.py
26
+ .spyderproject
27
+ .spyproject
28
+ .spyproject.*
29
+ ropeproject/
30
+ /site-packages/
31
+ .tox/
32
+ .coverage.xml
33
+ nosetests.xml
34
+ test/reports/
35
+ *.cover
36
+ *.log
37
+ .pytest_cache/
38
+ .pydevtree
39
+ .vscode/
40
+ .idea/
41
+ .DS_Store
42
+ .DS_Store?
43
+ ._*
44
+ .Spotlight-V100
45
+ .Trashes
46
+ ehthumbs.db
47
+ Thumbs.db
BACKEND_ARCHITECTURE.md ADDED
@@ -0,0 +1,388 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ## Directory Structure
2
+ ```
3
+ Directory of D:\VScode\GitHub\From_Console_to_Cloud\backend
4
+
5
+ __pycache__/
6
+ auth/
7
+ database/
8
+ models/
9
+ routes/
10
+ schemas/
11
+ .env
12
+ .gitignore
13
+ Dockerfile
14
+ auth.db
15
+ deploy_hf.sh
16
+ main.py
17
+ models.py
18
+ requirements.txt
19
+ space.yaml
20
+ update_neon_schema.sql
21
+ ```
22
+
23
+ # auth\jwt.py
24
+ ```python
25
+ import logging
26
+ from fastapi import Depends, HTTPException
27
+ from fastapi.security import HTTPBearer
28
+ from sqlmodel import Session, select
29
+ from database import get_session
30
+ import sqlalchemy
31
+ from datetime import datetime
32
+
33
+ logger = logging.getLogger(__name__)
34
+
35
+ security = HTTPBearer()
36
+
37
+ def get_current_user_id(
38
+ creds = Depends(security),
39
+ db: Session = Depends(get_session)
40
+ ) -> str:
41
+ token = creds.credentials
42
+
43
+ try:
44
+ # Check the 'session' table that Better Auth created
45
+ # Look for a session that matches the token and hasn't expired
46
+ result = db.execute(
47
+ sqlalchemy.text('SELECT "userId", "expiresAt" FROM "session" WHERE "token" = :t'),
48
+ {"t": token}
49
+ ).fetchone()
50
+
51
+ if not result:
52
+ logger.warning(f"Session not found for token: {token[:10]}...")
53
+ raise Exception("Session invalid")
54
+
55
+ user_id, expires_at = result
56
+
57
+ # Convert expires_at to datetime if it's a string
58
+ if isinstance(expires_at, str):
59
+ from datetime import datetime
60
+ expires_at = datetime.fromisoformat(expires_at.replace('Z', '+00:00'))
61
+
62
+ # Check if expired
63
+ if expires_at < datetime.now():
64
+ logger.warning("Session expired")
65
+ raise Exception("Session expired")
66
+
67
+ logger.info(f"Authenticated user: {user_id}")
68
+ return str(user_id)
69
+
70
+ except Exception as e:
71
+ logger.error(f"Auth failed: {str(e)}")
72
+ raise HTTPException(status_code=401, detail="Not authenticated")
73
+ ```
74
+
75
+ # database\__init__.py
76
+ ```python
77
+ import logging
78
+ from sqlmodel import Session, create_engine, SQLModel
79
+ import os
80
+ from dotenv import load_dotenv
81
+
82
+ logger = logging.getLogger(__name__)
83
+ load_dotenv()
84
+
85
+
86
+ # Database setup
87
+ DATABASE_URL = os.getenv("DATABASE_URL")
88
+ if not DATABASE_URL:
89
+ raise RuntimeError("DATABASE_URL is missing")
90
+
91
+ logger.info(f"Connecting to database: {DATABASE_URL.replace('@', '[@]').replace(':', '[:]') if DATABASE_URL else 'None'}")
92
+
93
+ engine = create_engine(DATABASE_URL, echo=True)
94
+
95
+ def create_db_and_tables():
96
+ """Create database tables"""
97
+ logger.info("Creating database tables...")
98
+ try:
99
+ SQLModel.metadata.create_all(engine)
100
+ logger.info("Database tables created successfully")
101
+ except Exception as e:
102
+ logger.error(f"Error creating database tables: {str(e)}")
103
+ raise
104
+
105
+ def get_session():
106
+ logger.debug("Opening database session")
107
+ with Session(engine) as session:
108
+ try:
109
+ yield session
110
+ except Exception as e:
111
+ logger.error(f"Error in database session: {str(e)}")
112
+ raise
113
+ finally:
114
+ logger.debug("Closing database session")
115
+ ```
116
+
117
+ # main.py
118
+ ```python
119
+ import os
120
+ import logging
121
+ from fastapi import FastAPI
122
+ from fastapi.middleware.cors import CORSMiddleware
123
+ from routes import tasks
124
+ from database import create_db_and_tables
125
+ from dotenv import load_dotenv
126
+
127
+ # Configure logging
128
+ logging.basicConfig(level=logging.INFO)
129
+ logger = logging.getLogger(__name__)
130
+
131
+ # Load environment variables
132
+ load_dotenv()
133
+
134
+ app = FastAPI(title="Todo API on Hugging Face")
135
+
136
+ app.add_middleware(
137
+ CORSMiddleware,
138
+ allow_origins=["http://localhost:3000", "https://console-to-cloud.netlify.app"],
139
+ allow_credentials=True,
140
+ allow_methods=["*"],
141
+ allow_headers=["*"],
142
+ )
143
+
144
+ app.include_router(tasks.router)
145
+
146
+ @app.on_event("startup")
147
+ def startup():
148
+ logger.info("Starting up the application...")
149
+ try:
150
+ create_db_and_tables()
151
+ logger.info("Database tables created successfully")
152
+ except Exception as e:
153
+ logger.error(f"Error creating database tables: {e}")
154
+ raise
155
+
156
+ @app.get("/")
157
+ def read_root():
158
+ logger.info("Root endpoint accessed")
159
+ return {"message": "Todo API running on Hugging Face Spaces!"}
160
+
161
+ @app.get("/health")
162
+ def health_check():
163
+ logger.info("Health check endpoint accessed")
164
+ return {"status": "healthy"}
165
+
166
+ # For Hugging Face Spaces
167
+ if __name__ == "__main__":
168
+ import uvicorn
169
+ logger.info("Starting Uvicorn server...")
170
+ uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
171
+ ```
172
+
173
+ # models.py
174
+ ```python
175
+ from sqlmodel import SQLModel, Field
176
+ from typing import Optional, List
177
+ from datetime import datetime
178
+ from enum import Enum
179
+ from sqlalchemy import JSON
180
+
181
+
182
+ class TaskPriority(str, Enum):
183
+ low = "low"
184
+ medium = "medium"
185
+ high = "high"
186
+
187
+
188
+ class Task(SQLModel, table=True):
189
+ id: Optional[int] = Field(default=None, primary_key=True)
190
+ user_id: str = Field(index=True)
191
+ title: str
192
+ description: Optional[str] = None
193
+ completed: bool = False
194
+ priority: TaskPriority = TaskPriority.medium
195
+ category: Optional[str] = None
196
+ tags: List[str] = Field(default_factory=list, sa_type=JSON)
197
+ created_at: datetime = Field(default_factory=datetime.utcnow)
198
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
199
+ ```
200
+
201
+ # models\user.py
202
+ ```python
203
+ from sqlmodel import SQLModel, Field
204
+ from typing import Optional
205
+ from datetime import datetime
206
+
207
+ class User(SQLModel, table=True):
208
+ id: Optional[str] = Field(default=None, primary_key=True)
209
+ email: str = Field(unique=True, index=True)
210
+ name: str
211
+ password_hash: str
212
+ created_at: datetime = Field(default_factory=datetime.utcnow)
213
+ ```
214
+
215
+ # routes\tasks.py
216
+ ```python
217
+ import logging
218
+ from fastapi import APIRouter, Depends, HTTPException
219
+ from sqlmodel import Session, select
220
+ from datetime import datetime
221
+ from models import Task
222
+ from schemas.tasks import TaskCreate, TaskUpdate, TaskResponse
223
+ from database import get_session
224
+ from auth.jwt import get_current_user_id
225
+
226
+ logger = logging.getLogger(__name__)
227
+
228
+ router = APIRouter(prefix="/api", tags=["tasks"])
229
+
230
+
231
+ @router.get("/tasks")
232
+ def list_tasks(
233
+ session: Session = Depends(get_session),
234
+ user_id: str = Depends(get_current_user_id),
235
+ ):
236
+ logger.info(f"Fetching tasks for user_id: {user_id}")
237
+ try:
238
+ tasks = session.exec(
239
+ select(Task).where(Task.user_id == user_id)
240
+ ).all()
241
+ logger.info(f"Found {len(tasks)} tasks for user_id: {user_id}")
242
+ return {"data": tasks}
243
+ except Exception as e:
244
+ logger.error(f"Error fetching tasks for user_id {user_id}: {str(e)}")
245
+ raise
246
+
247
+
248
+ @router.post("/tasks")
249
+ def create_task(
250
+ task: TaskCreate,
251
+ session: Session = Depends(get_session),
252
+ user_id: str = Depends(get_current_user_id),
253
+ ):
254
+ logger.info(f"Creating task for user_id: {user_id}, task data: {task}")
255
+ try:
256
+ db_task = Task(**task.dict(), user_id=user_id)
257
+ session.add(db_task)
258
+ session.commit()
259
+ session.refresh(db_task)
260
+ logger.info(f"Created task with id: {db_task.id} for user_id: {user_id}")
261
+ return {"data": db_task}
262
+ except Exception as e:
263
+ logger.error(f"Error creating task for user_id {user_id}: {str(e)}")
264
+ raise
265
+
266
+
267
+ @router.put("/tasks/{task_id}")
268
+ def update_task(
269
+ task_id: int,
270
+ updates: TaskUpdate,
271
+ session: Session = Depends(get_session),
272
+ user_id: str = Depends(get_current_user_id),
273
+ ):
274
+ logger.info(f"Updating task {task_id} for user_id: {user_id}, updates: {updates}")
275
+ try:
276
+ task = session.get(Task, task_id)
277
+ if not task or task.user_id != user_id:
278
+ logger.warning(f"Task {task_id} not found or user_id mismatch for user_id: {user_id}")
279
+ raise HTTPException(status_code=404)
280
+
281
+ for k, v in updates.dict(exclude_unset=True).items():
282
+ setattr(task, k, v)
283
+
284
+ task.updated_at = datetime.utcnow()
285
+ session.commit()
286
+ session.refresh(task)
287
+ logger.info(f"Updated task {task_id} successfully")
288
+ return {"data": task}
289
+ except HTTPException:
290
+ raise
291
+ except Exception as e:
292
+ logger.error(f"Error updating task {task_id} for user_id {user_id}: {str(e)}")
293
+ raise
294
+
295
+
296
+ @router.delete("/tasks/{task_id}")
297
+ def delete_task(
298
+ task_id: int,
299
+ session: Session = Depends(get_session),
300
+ user_id: str = Depends(get_current_user_id),
301
+ ):
302
+ logger.info(f"Deleting task {task_id} for user_id: {user_id}")
303
+ try:
304
+ task = session.get(Task, task_id)
305
+ if not task or task.user_id != user_id:
306
+ logger.warning(f"Task {task_id} not found or user_id mismatch for user_id: {user_id}")
307
+ raise HTTPException(status_code=404)
308
+
309
+ session.delete(task)
310
+ session.commit()
311
+ logger.info(f"Deleted task {task_id} successfully")
312
+ return {"data": {"ok": True}}
313
+ except HTTPException:
314
+ raise
315
+ except Exception as e:
316
+ logger.error(f"Error deleting task {task_id} for user_id {user_id}: {str(e)}")
317
+ raise
318
+
319
+
320
+ @router.patch("/tasks/{task_id}/complete")
321
+ def toggle_complete(
322
+ task_id: int,
323
+ session: Session = Depends(get_session),
324
+ user_id: str = Depends(get_current_user_id),
325
+ ):
326
+ logger.info(f"Toggling completion for task {task_id} for user_id: {user_id}")
327
+ try:
328
+ task = session.get(Task, task_id)
329
+ if not task or task.user_id != user_id:
330
+ logger.warning(f"Task {task_id} not found or user_id mismatch for user_id: {user_id}")
331
+ raise HTTPException(status_code=404)
332
+
333
+ task.completed = not task.completed
334
+ task.updated_at = datetime.utcnow()
335
+ session.commit()
336
+ session.refresh(task)
337
+ logger.info(f"Toggled completion for task {task_id}, now completed: {task.completed}")
338
+ return {"data": task}
339
+ except HTTPException:
340
+ raise
341
+ except Exception as e:
342
+ logger.error(f"Error toggling completion for task {task_id} for user_id {user_id}: {str(e)}")
343
+ raise
344
+ ```
345
+
346
+ # schemas\tasks.py
347
+ ```python
348
+ from pydantic import BaseModel, Field
349
+ from typing import Optional, List
350
+ from datetime import datetime
351
+ from enum import Enum
352
+
353
+
354
+ class TaskPriority(str, Enum):
355
+ low = "low"
356
+ medium = "medium"
357
+ high = "high"
358
+
359
+
360
+ class TaskBase(BaseModel):
361
+ title: str
362
+ description: Optional[str] = None
363
+ priority: TaskPriority = TaskPriority.medium
364
+ category: Optional[str] = None
365
+ tags: List[str] = Field(default_factory=list)
366
+
367
+
368
+ class TaskCreate(TaskBase):
369
+ pass
370
+
371
+
372
+ class TaskUpdate(BaseModel):
373
+ title: Optional[str] = None
374
+ description: Optional[str] = None
375
+ priority: Optional[TaskPriority] = None
376
+ category: Optional[str] = None
377
+ tags: Optional[List[str]] = None
378
+ completed: Optional[bool] = None
379
+
380
+
381
+ class TaskResponse(TaskBase):
382
+ id: int
383
+ user_id: str
384
+ completed: bool
385
+ created_at: datetime
386
+ updated_at: datetime
387
+ ```
388
+
CLAUDE.md ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Backend Guidelines (FastAPI Application)
2
+
3
+ ## Stack
4
+ - FastAPI
5
+ - SQLModel (ORM)
6
+ - Pydantic (data validation)
7
+ - JWT (authentication)
8
+ - Neon PostgreSQL
9
+
10
+ ## Project Structure
11
+ - `main.py` - FastAPI app entry point
12
+ - `models.py` - SQLModel database models
13
+ - `schemas/` - Pydantic models for request/response validation
14
+ - `routes/` - API route handlers
15
+ - `auth/` - Authentication and JWT middleware
16
+ - `database/` - Database connection and session management
17
+ - `dependencies/` - FastAPI dependencies
18
+
19
+ ## API Conventions
20
+ - All routes under `/api/{user_id}/` for user context
21
+ - Return JSON responses using Pydantic models
22
+ - Handle errors with HTTPException
23
+ - Use proper HTTP status codes (200, 201, 400, 401, 403, 404, 500)
24
+ - Include JWT authentication on all protected endpoints
25
+
26
+ ## Database
27
+ - Use SQLModel for all database operations
28
+ - Connection string from environment variable: DATABASE_URL
29
+ - Implement proper indexing for performance
30
+ - Use database sessions via dependency injection
31
+
32
+ ## Authentication
33
+ - Implement JWT token verification middleware
34
+ - Validate that user_id in URL matches authenticated user
35
+ - Return 401 for invalid tokens
36
+ - Return 403 for unauthorized access attempts
37
+ - Store user_id in token for authorization
38
+
39
+ ## Running
40
+ - Development: `uvicorn main:app --reload --port 8000`
41
+ - Production: `uvicorn main:app --host 0.0.0.0 --port 8000`
42
+
43
+ ## Security
44
+ - Validate user_id matches authenticated user on all endpoints
45
+ - Implement proper input validation with Pydantic
46
+ - Use parameterized queries to prevent SQL injection
47
+ - Implement rate limiting if needed
Dockerfile ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ FROM python:3.10-slim
2
+
3
+ # Install uv
4
+ RUN pip install --upgrade pip && pip install uv
5
+
6
+ WORKDIR /app
7
+
8
+ COPY requirements.txt .
9
+ RUN uv pip install -r requirements.txt
10
+
11
+ COPY . .
12
+
13
+ EXPOSE 7860
14
+
15
+ CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860"]
README.md CHANGED
@@ -1,11 +1,25 @@
1
  ---
2
- title: Todo Fastapi
3
- emoji: 🐢
4
- colorFrom: green
5
- colorTo: gray
6
  sdk: docker
7
- pinned: false
8
- short_description: MuhammedSuhaib/todo-fastapi
9
  ---
10
 
11
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  ---
2
+ title: Todo API
3
+ emoji:
4
+ colorFrom: indigo
5
+ colorTo: blue
6
  sdk: docker
7
+ app_port: 7860
 
8
  ---
9
 
10
+ # Todo Backend API
11
+ FastAPI backend for the Todo application.
12
+ This is a FastAPI backend for a todo application, deployed on Hugging Face Spaces using Docker.
13
+
14
+ ## API Endpoints
15
+
16
+ - `GET /api/tasks` - Get all tasks for the authenticated user
17
+ - `POST /api/tasks` - Create a new task
18
+ - `PUT /api/tasks/{task_id}` - Update a task
19
+ - `DELETE /api/tasks/{task_id}` - Delete a task
20
+ - `PATCH /api/tasks/{task_id}/complete` - Toggle task completion status
21
+
22
+ ## Environment Variables
23
+
24
+ - `DATABASE_URL`: PostgreSQL database URL (Neon)
25
+ - `BETTER_AUTH_SECRET`: Secret for JWT token verification
auth.db ADDED
Binary file (12.3 kB). View file
 
auth/jwt.py ADDED
@@ -0,0 +1,43 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from fastapi import Depends, HTTPException
3
+ from fastapi.security import HTTPBearer
4
+ from sqlmodel import Session
5
+ from database import get_session
6
+ import sqlalchemy
7
+ from datetime import datetime, timezone
8
+
9
+ logger = logging.getLogger(__name__)
10
+ security = HTTPBearer()
11
+
12
+ def get_current_user_id(
13
+ creds = Depends(security),
14
+ db: Session = Depends(get_session)
15
+ ) -> str:
16
+ token = creds.credentials
17
+
18
+ try:
19
+ # We query the session table directly. Better Auth stores tokens as-is.
20
+ # userId and expiresAt are standard Better Auth columns.
21
+ query = sqlalchemy.text('SELECT "userId", "expiresAt" FROM "session" WHERE "token" = :t')
22
+ result = db.execute(query, {"t": token}).fetchone()
23
+
24
+ if not result:
25
+ logger.warning(f"Invalid session token attempted: {token[:10]}")
26
+ raise HTTPException(status_code=401, detail="Invalid session")
27
+
28
+ user_id, expires_at = result
29
+
30
+ # Check if the session has expired
31
+ # Ensure timezone comparison is consistent
32
+ if expires_at.replace(tzinfo=timezone.utc) < datetime.now(timezone.utc):
33
+ logger.warning(f"Session expired for user: {user_id}")
34
+ raise HTTPException(status_code=401, detail="Session expired")
35
+
36
+ logger.info(f"User {user_id} authenticated successfully")
37
+ return str(user_id)
38
+
39
+ except HTTPException:
40
+ raise
41
+ except Exception as e:
42
+ logger.error(f"Auth System Error: {str(e)}")
43
+ raise HTTPException(status_code=401, detail="Internal authentication failure")
database/__init__.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from sqlmodel import Session, create_engine, SQLModel
3
+ import os
4
+ from dotenv import load_dotenv
5
+
6
+ logger = logging.getLogger(__name__)
7
+ load_dotenv()
8
+
9
+
10
+ # Database setup
11
+ DATABASE_URL = os.getenv("DATABASE_URL")
12
+ if not DATABASE_URL:
13
+ raise RuntimeError("DATABASE_URL is missing")
14
+
15
+ logger.info(f"Connecting to database: {DATABASE_URL.replace('@', '[@]').replace(':', '[:]') if DATABASE_URL else 'None'}")
16
+
17
+ engine = create_engine(DATABASE_URL, echo=True)
18
+
19
+ def create_db_and_tables():
20
+ """Create database tables"""
21
+ logger.info("Creating database tables...")
22
+ try:
23
+ SQLModel.metadata.create_all(engine)
24
+ logger.info("Database tables created successfully")
25
+ except Exception as e:
26
+ logger.error(f"Error creating database tables: {str(e)}")
27
+ raise
28
+
29
+ def get_session():
30
+ logger.debug("Opening database session")
31
+ with Session(engine) as session:
32
+ try:
33
+ yield session
34
+ except Exception as e:
35
+ logger.error(f"Error in database session: {str(e)}")
36
+ raise
37
+ finally:
38
+ logger.debug("Closing database session")
deploy_hf.sh ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/bin/bash
2
+
3
+ # Script to deploy the backend to Hugging Face Spaces
4
+
5
+ echo "Preparing backend for Hugging Face Spaces deployment..."
6
+
7
+ # Create a zip file with all necessary files
8
+ cd ../..
9
+ zip -r backend-hf-deploy.zip backend/
10
+
11
+ echo "Deployment package created: backend-hf-deploy.zip"
12
+
13
+ echo "To deploy to Hugging Face Spaces:"
14
+ echo "1. Go to https://huggingface.co/spaces"
15
+ echo "2. Click 'Create Space'"
16
+ echo "3. Select 'Docker' SDK and 'No secrets' (add secrets later in Space settings)"
17
+ echo "4. Upload the backend-hf-deploy.zip file or clone this repository"
18
+ echo "5. Add the following environment variables in Space settings:"
19
+ echo " - DATABASE_URL (your Neon database URL)"
20
+ echo " - BETTER_AUTH_SECRET (your Better Auth secret)"
21
+ echo " - FASTAPI_ALGORITHM"
22
+ echo " - FASTAPI_ACCESS_TOKEN_EXPIRE_MINUTES"
23
+
24
+ echo ""
25
+ echo "Alternatively, you can use the Hugging Face CLI:"
26
+ echo "1. Install: pip install huggingface_hub"
27
+ echo "2. Login: huggingface-cli login"
28
+ echo "3. Create space: hf_hub create-space your-username/todo-backend -r docker"
29
+ echo "4. Upload files to the space repository"
main.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import logging
3
+ from fastapi import FastAPI
4
+ from fastapi.middleware.cors import CORSMiddleware
5
+ from routes import tasks
6
+ from database import create_db_and_tables
7
+ from dotenv import load_dotenv
8
+
9
+ # Configure logging
10
+ logging.basicConfig(level=logging.INFO)
11
+ logger = logging.getLogger(__name__)
12
+
13
+ # Load environment variables
14
+ load_dotenv()
15
+
16
+ app = FastAPI(title="Todo API on Hugging Face")
17
+
18
+ app.add_middleware(
19
+ CORSMiddleware,
20
+ allow_origins=["http://localhost:3000", "https://console-to-cloud.netlify.app"],
21
+ allow_credentials=True,
22
+ allow_methods=["*"],
23
+ allow_headers=["*"],
24
+ )
25
+
26
+ app.include_router(tasks.router)
27
+
28
+ @app.on_event("startup")
29
+ def startup():
30
+ logger.info("Starting up the application...")
31
+ try:
32
+ create_db_and_tables()
33
+ logger.info("Database tables created successfully")
34
+ except Exception as e:
35
+ logger.error(f"Error creating database tables: {e}")
36
+ raise
37
+
38
+ @app.get("/")
39
+ def read_root():
40
+ logger.info("Root endpoint accessed")
41
+ return {"message": "Todo API running on Hugging Face Spaces!"}
42
+
43
+ @app.get("/health")
44
+ def health_check():
45
+ logger.info("Health check endpoint accessed")
46
+ return {"status": "healthy"}
47
+
48
+ # For Hugging Face Spaces
49
+ if __name__ == "__main__":
50
+ import uvicorn
51
+ logger.info("Starting Uvicorn server...")
52
+ uvicorn.run(app, host="0.0.0.0", port=int(os.environ.get("PORT", 7860)))
models.py ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field
2
+ from typing import Optional, List
3
+ from datetime import datetime
4
+ from enum import Enum
5
+ from sqlalchemy import JSON
6
+
7
+
8
+ class TaskPriority(str, Enum):
9
+ low = "low"
10
+ medium = "medium"
11
+ high = "high"
12
+
13
+
14
+ class Task(SQLModel, table=True):
15
+ id: Optional[int] = Field(default=None, primary_key=True)
16
+ user_id: str = Field(index=True)
17
+ title: str
18
+ description: Optional[str] = None
19
+ completed: bool = False
20
+ priority: TaskPriority = TaskPriority.medium
21
+ category: Optional[str] = None
22
+ tags: List[str] = Field(default_factory=list, sa_type=JSON)
23
+ created_at: datetime = Field(default_factory=datetime.utcnow)
24
+ updated_at: datetime = Field(default_factory=datetime.utcnow)
models/user.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ from sqlmodel import SQLModel, Field
2
+ from typing import Optional
3
+ from datetime import datetime
4
+
5
+ class User(SQLModel, table=True):
6
+ id: Optional[str] = Field(default=None, primary_key=True)
7
+ email: str = Field(unique=True, index=True)
8
+ name: str
9
+ password_hash: str
10
+ created_at: datetime = Field(default_factory=datetime.utcnow)
requirements.txt ADDED
@@ -0,0 +1,13 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ fastapi[standard]
2
+ uvicorn[standard]
3
+ sqlmodel
4
+ pydantic
5
+ python-jose[cryptography]
6
+ passlib[bcrypt]
7
+ python-multipart
8
+ better-auth
9
+ python-dotenv
10
+ asyncpg
11
+ alembic
12
+ psycopg2-binary
13
+ requests
routes/tasks.py ADDED
@@ -0,0 +1,127 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import logging
2
+ from fastapi import APIRouter, Depends, HTTPException
3
+ from sqlmodel import Session, select
4
+ from datetime import datetime
5
+ from models import Task
6
+ from schemas.tasks import TaskCreate, TaskUpdate, TaskResponse
7
+ from database import get_session
8
+ from auth.jwt import get_current_user_id
9
+
10
+ logger = logging.getLogger(__name__)
11
+
12
+ router = APIRouter(prefix="/api", tags=["tasks"])
13
+
14
+
15
+ @router.get("/tasks")
16
+ def list_tasks(
17
+ session: Session = Depends(get_session),
18
+ user_id: str = Depends(get_current_user_id),
19
+ ):
20
+ logger.info(f"Fetching tasks for user_id: {user_id}")
21
+ try:
22
+ tasks = session.exec(
23
+ select(Task).where(Task.user_id == user_id)
24
+ ).all()
25
+ logger.info(f"Found {len(tasks)} tasks for user_id: {user_id}")
26
+ return {"data": tasks}
27
+ except Exception as e:
28
+ logger.error(f"Error fetching tasks for user_id {user_id}: {str(e)}")
29
+ raise
30
+
31
+
32
+ @router.post("/tasks")
33
+ def create_task(
34
+ task: TaskCreate,
35
+ session: Session = Depends(get_session),
36
+ user_id: str = Depends(get_current_user_id),
37
+ ):
38
+ logger.info(f"Creating task for user_id: {user_id}, task data: {task}")
39
+ try:
40
+ db_task = Task(**task.dict(), user_id=user_id)
41
+ session.add(db_task)
42
+ session.commit()
43
+ session.refresh(db_task)
44
+ logger.info(f"Created task with id: {db_task.id} for user_id: {user_id}")
45
+ return {"data": db_task}
46
+ except Exception as e:
47
+ logger.error(f"Error creating task for user_id {user_id}: {str(e)}")
48
+ raise
49
+
50
+
51
+ @router.put("/tasks/{task_id}")
52
+ def update_task(
53
+ task_id: int,
54
+ updates: TaskUpdate,
55
+ session: Session = Depends(get_session),
56
+ user_id: str = Depends(get_current_user_id),
57
+ ):
58
+ logger.info(f"Updating task {task_id} for user_id: {user_id}, updates: {updates}")
59
+ try:
60
+ task = session.get(Task, task_id)
61
+ if not task or task.user_id != user_id:
62
+ logger.warning(f"Task {task_id} not found or user_id mismatch for user_id: {user_id}")
63
+ raise HTTPException(status_code=404)
64
+
65
+ for k, v in updates.dict(exclude_unset=True).items():
66
+ setattr(task, k, v)
67
+
68
+ task.updated_at = datetime.utcnow()
69
+ session.commit()
70
+ session.refresh(task)
71
+ logger.info(f"Updated task {task_id} successfully")
72
+ return {"data": task}
73
+ except HTTPException:
74
+ raise
75
+ except Exception as e:
76
+ logger.error(f"Error updating task {task_id} for user_id {user_id}: {str(e)}")
77
+ raise
78
+
79
+
80
+ @router.delete("/tasks/{task_id}")
81
+ def delete_task(
82
+ task_id: int,
83
+ session: Session = Depends(get_session),
84
+ user_id: str = Depends(get_current_user_id),
85
+ ):
86
+ logger.info(f"Deleting task {task_id} for user_id: {user_id}")
87
+ try:
88
+ task = session.get(Task, task_id)
89
+ if not task or task.user_id != user_id:
90
+ logger.warning(f"Task {task_id} not found or user_id mismatch for user_id: {user_id}")
91
+ raise HTTPException(status_code=404)
92
+
93
+ session.delete(task)
94
+ session.commit()
95
+ logger.info(f"Deleted task {task_id} successfully")
96
+ return {"data": {"ok": True}}
97
+ except HTTPException:
98
+ raise
99
+ except Exception as e:
100
+ logger.error(f"Error deleting task {task_id} for user_id {user_id}: {str(e)}")
101
+ raise
102
+
103
+
104
+ @router.patch("/tasks/{task_id}/complete")
105
+ def toggle_complete(
106
+ task_id: int,
107
+ session: Session = Depends(get_session),
108
+ user_id: str = Depends(get_current_user_id),
109
+ ):
110
+ logger.info(f"Toggling completion for task {task_id} for user_id: {user_id}")
111
+ try:
112
+ task = session.get(Task, task_id)
113
+ if not task or task.user_id != user_id:
114
+ logger.warning(f"Task {task_id} not found or user_id mismatch for user_id: {user_id}")
115
+ raise HTTPException(status_code=404)
116
+
117
+ task.completed = not task.completed
118
+ task.updated_at = datetime.utcnow()
119
+ session.commit()
120
+ session.refresh(task)
121
+ logger.info(f"Toggled completion for task {task_id}, now completed: {task.completed}")
122
+ return {"data": task}
123
+ except HTTPException:
124
+ raise
125
+ except Exception as e:
126
+ logger.error(f"Error toggling completion for task {task_id} for user_id {user_id}: {str(e)}")
127
+ raise
schemas/tasks.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Optional, List
3
+ from datetime import datetime
4
+ from enum import Enum
5
+
6
+
7
+ class TaskPriority(str, Enum):
8
+ low = "low"
9
+ medium = "medium"
10
+ high = "high"
11
+
12
+
13
+ class TaskBase(BaseModel):
14
+ title: str
15
+ description: Optional[str] = None
16
+ priority: TaskPriority = TaskPriority.medium
17
+ category: Optional[str] = None
18
+ tags: List[str] = Field(default_factory=list)
19
+
20
+
21
+ class TaskCreate(TaskBase):
22
+ pass
23
+
24
+
25
+ class TaskUpdate(BaseModel):
26
+ title: Optional[str] = None
27
+ description: Optional[str] = None
28
+ priority: Optional[TaskPriority] = None
29
+ category: Optional[str] = None
30
+ tags: Optional[List[str]] = None
31
+ completed: Optional[bool] = None
32
+
33
+
34
+ class TaskResponse(TaskBase):
35
+ id: int
36
+ user_id: str
37
+ completed: bool
38
+ created_at: datetime
39
+ updated_at: datetime
space.yaml ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ api_version: 0.0.1
2
+ sdk: docker
update_neon_schema.sql ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ -- Phase II Neon schema (tasks only)
2
+ -- Backend is JWT-verify only, no user table
3
+
4
+ CREATE TABLE IF NOT EXISTS task (
5
+ id SERIAL PRIMARY KEY,
6
+ user_id TEXT NOT NULL,
7
+ title TEXT NOT NULL,
8
+ description TEXT,
9
+ completed BOOLEAN DEFAULT FALSE,
10
+ priority TEXT DEFAULT 'medium',
11
+ category TEXT,
12
+ tags JSONB DEFAULT '[]',
13
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
14
+ updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
15
+ );
16
+
17
+ CREATE INDEX IF NOT EXISTS idx_task_user_id ON task(user_id);