AhmadYarAI commited on
Commit
6d07ace
·
1 Parent(s): 8e359b7

Fix Neon DB connection (direct, non-pooler)

Browse files
Files changed (3) hide show
  1. app/core/config.py +9 -4
  2. app/routers/chat_ws.py +28 -0
  3. main.py +3 -376
app/core/config.py CHANGED
@@ -1,13 +1,18 @@
1
  from pydantic_settings import BaseSettings, SettingsConfigDict
2
 
3
  class Settings(BaseSettings):
4
- # DB
5
  DATABASE_URL: str
6
-
7
- # JWT config (names MUST match what your code uses)
8
  JWT_SECRET: str = "changeme"
9
  JWT_ALGORITHM: str = "HS256"
10
  ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
11
- model_config = SettingsConfigDict(env_file=".env", env_file_encoding="utf-8")
 
 
 
 
 
12
 
13
  settings = Settings()
 
1
  from pydantic_settings import BaseSettings, SettingsConfigDict
2
 
3
  class Settings(BaseSettings):
4
+ # Database
5
  DATABASE_URL: str
6
+
7
+ # JWT
8
  JWT_SECRET: str = "changeme"
9
  JWT_ALGORITHM: str = "HS256"
10
  ACCESS_TOKEN_EXPIRE_MINUTES: int = 60
11
+
12
+ model_config = SettingsConfigDict(
13
+ env_file=".env",
14
+ env_file_encoding="utf-8",
15
+ extra="allow"
16
+ )
17
 
18
  settings = Settings()
app/routers/chat_ws.py ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ from fastapi import APIRouter, WebSocket, WebSocketDisconnect
3
+
4
+ router = APIRouter()
5
+
6
+ @router.websocket("/ws/chat")
7
+ async def chat_socket(websocket: WebSocket):
8
+ await websocket.accept()
9
+ print("WebSocket client connected")
10
+
11
+ try:
12
+ while True:
13
+ data = await websocket.receive_text()
14
+ payload = json.loads(data)
15
+
16
+ if payload.get("type") == "user_message":
17
+ # Temporary echo (no OpenAI yet)
18
+ await websocket.send_text(json.dumps({
19
+ "type": "token",
20
+ "content": f"Echo: {payload['content']}"
21
+ }))
22
+
23
+ await websocket.send_text(json.dumps({
24
+ "type": "end"
25
+ }))
26
+
27
+ except WebSocketDisconnect:
28
+ print("WebSocket client disconnected")
main.py CHANGED
@@ -10,7 +10,8 @@ from app.routers import family
10
  from app.routers import expense
11
  from app.routers import categorybudget
12
  from app.routers import budget
13
- from openai import OpenAI
 
14
  # create missing tables (won't alter existing columns)
15
  Base.metadata.create_all(bind=engine)
16
 
@@ -42,384 +43,10 @@ app.include_router(family.router)
42
  app.include_router(expense.router)
43
  app.include_router(categorybudget.router)
44
  app.include_router(budget.router)
 
45
  # app.include_router(users.router)
46
  # app.include_router(posts.router)
47
  # app.include_router(comments.router)
48
  # app.include_router(likes.router)
49
 
50
 
51
- # # ----------------- MySQL Connection -----------------
52
- # DATABASE_URL = "mysql+pymysql://root@127.0.0.1:3306/city_university"
53
- # # DATABASE_URL = "postgresql+psycopg2://city_university_db_user:au84DXp5L55SYrir23DzrezulwqSJZzc@dpg-d2gitojuibrs73ed7s00-a.oregon-postgres.render.com:5432/city_university_db"
54
- # engine = create_engine(DATABASE_URL, pool_pre_ping=True)
55
- # SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
56
- # # ----------------- ORM Model (matches user_accounts table) -----------------
57
-
58
-
59
- # app = FastAPI()
60
- # router = APIRouter()
61
-
62
-
63
-
64
- # Base = declarative_base()
65
- # UPLOAD_DIR = "uploads"
66
- # os.makedirs(UPLOAD_DIR, exist_ok=True)
67
- # app.mount("/uploads", StaticFiles(directory=UPLOAD_DIR), name="uploads")
68
-
69
-
70
- # app.add_middleware(
71
- # CORSMiddleware,
72
- # allow_origins=["*"], # allow all origins
73
- # allow_credentials=False, # must be False when using "*"
74
- # allow_methods=["*"], # GET, POST, PUT, DELETE, OPTIONS, ...
75
- # allow_headers=["*"], # Authorization, Content-Type, etc.
76
- # )
77
-
78
-
79
- # class UserDB(Base):
80
- # __tablename__ = "user_accounts"
81
- # id = Column(Integer, primary_key=True, index=True)
82
- # full_name = Column(String(100), nullable=False)
83
- # email = Column(String(100), unique=True, index=True, nullable=False)
84
- # password = Column(String(255), nullable=False)
85
-
86
- # # Dependency for getting DB session in routes
87
- # def get_db():
88
- # db = SessionLocal()
89
- # try:
90
- # yield db
91
- # finally:
92
- # db.close()
93
-
94
- # # ----------------- Request Body Model -----------------
95
- # class UserSignup(BaseModel):
96
- # full_name: str
97
- # email: EmailStr
98
- # password: str
99
-
100
- # class UserLogin(BaseModel):
101
- # email: EmailStr
102
- # password: str
103
-
104
- # class ResetPassword(BaseModel):
105
- # email: EmailStr
106
- # new_password: str
107
-
108
- # class UserOut(BaseModel):
109
- # id: int
110
- # full_name: str
111
- # email: EmailStr
112
- # model_config = ConfigDict(from_attributes=True) # pydantic v2
113
-
114
- # class UserLite(BaseModel):
115
- # id: int
116
- # full_name: str
117
- # model_config = ConfigDict(from_attributes=True)
118
-
119
-
120
- # class LikeRequest(BaseModel):
121
- # user_id: int
122
-
123
-
124
- # class PostOut(PModel):
125
- # id: int
126
- # user_id: int
127
- # content: Optional[str] = None
128
- # image_url: Optional[str] = None
129
- # created_at: datetime
130
- # class Config:
131
- # from_attributes = True # pydantic v2
132
-
133
-
134
- # class PostDB(Base):
135
- # __tablename__ = "posts"
136
- # id = Column(Integer, primary_key=True, index=True)
137
- # user_id = Column(Integer, ForeignKey("user_accounts.id", ondelete="CASCADE"), nullable=False)
138
- # content = Column(Text, nullable=True)
139
- # image_url = Column(String(300), nullable=True)
140
- # created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
141
-
142
- # user = relationship("UserDB")
143
- # comments = relationship("CommentDB", backref="post", cascade="all, delete-orphan")
144
- # likes = relationship("LikeDB", back_populates="post", cascade="all, delete-orphan")
145
-
146
- # class CommentDB(Base):
147
- # __tablename__ = "comments"
148
- # id = Column(Integer, primary_key=True, index=True)
149
- # content = Column(Text, nullable=False)
150
- # created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
151
- # post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), nullable=False)
152
- # user_id = Column(Integer, ForeignKey("user_accounts.id", ondelete="CASCADE"), nullable=False)
153
- # user = relationship("UserDB") # <-- needed
154
-
155
- # class LikeDB(Base):
156
- # __tablename__ = "likes"
157
- # id = Column(Integer, primary_key=True, index=True)
158
- # user_id = Column(Integer, ForeignKey("user_accounts.id", ondelete="CASCADE"), nullable=False)
159
- # post_id = Column(Integer, ForeignKey("posts.id", ondelete="CASCADE"), nullable=False)
160
- # created_at = Column(DateTime, default=datetime.utcnow, nullable=False)
161
-
162
- # __table_args__ = (UniqueConstraint('user_id', 'post_id', name='unique_like'),)
163
-
164
- # user = relationship("UserDB")
165
- # post = relationship("PostDB", back_populates="likes")
166
-
167
- # class CommentCreate(BaseModel):
168
- # content: str
169
- # post_id: int
170
- # user_id: int
171
-
172
- # class CommentOut(BaseModel):
173
- # id: int
174
- # content: str
175
- # user_id: int
176
- # post_id: int
177
- # created_at: datetime
178
- # user: UserLite
179
- # model_config = ConfigDict(from_attributes=True)
180
-
181
- # class PostWithComments(BaseModel):
182
- # id: int
183
- # user_id: int
184
- # content: Optional[str] = None
185
- # image_url: Optional[str] = None
186
- # created_at: datetime
187
- # user: UserLite # 👈 include author here
188
- # comments: List[CommentOut] = [] # include comments array
189
- # like_count: int = 0 # 👈 total likes
190
- # is_liked_by_user: bool | None = None # 👈 viewer-specific
191
- # model_config = ConfigDict(from_attributes=True)
192
-
193
-
194
- # @app.get("/")
195
- # def home():
196
- # return {"message": "Hello FastAPI!"}
197
-
198
- # @app.post("/signup")
199
- # def signup(user: UserSignup, db: Session = Depends(get_db)):
200
- # # Check if email exists
201
- # if db.query(UserDB).filter(UserDB.email == user.email).first():
202
- # raise HTTPException(status_code=400, detail="Email already registered")
203
-
204
- # # Hash password
205
- # hashed_pw = bcrypt.hashpw(user.password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
206
-
207
- # # Create new user
208
- # new_user = UserDB(full_name=user.full_name, email=user.email, password=hashed_pw)
209
- # db.add(new_user)
210
- # db.commit()
211
- # db.refresh(new_user)
212
-
213
- # return new_user
214
-
215
-
216
- # @app.post("/login", response_model=UserOut)
217
- # def login(user: UserLogin, db: Session = Depends(get_db)):
218
- # db_user = db.query(UserDB).filter(UserDB.email == user.email).first()
219
- # if not db_user or not bcrypt.checkpw(user.password.encode('utf-8'), db_user.password.encode('utf-8')):
220
- # raise HTTPException(status_code=400, detail="Invalid email or password")
221
- # return db_user # << return the user, not {"message": ...}
222
-
223
- # @app.post("/forgot-password")
224
- # def forgot_password(reset: ResetPassword, db: Session = Depends(get_db)):
225
- # db_user = db.query(UserDB).filter(UserDB.email == reset.email).first()
226
- # if not db_user:
227
- # raise HTTPException(status_code=404, detail="Email not found")
228
-
229
- # hashed_pw = bcrypt.hashpw(reset.new_password.encode('utf-8'), bcrypt.gensalt()).decode('utf-8')
230
- # db_user.password = hashed_pw
231
- # db.commit()
232
- # return {"message": "Password updated successfully"}
233
-
234
- # @app.post("/posts", response_model=PostWithComments, status_code=201)
235
- # async def create_post(
236
- # user_id: int = Form(...),
237
- # content: Optional[str] = Form(None),
238
- # image: Optional[UploadFile] = File(None),
239
- # db: Session = Depends(get_db),
240
- # ):
241
- # if (not content or not content.strip()) and image is None:
242
- # raise HTTPException(status_code=400, detail="Provide content or an image")
243
-
244
- # user = db.get(UserDB, user_id)
245
- # if not user:
246
- # raise HTTPException(status_code=404, detail="User not found")
247
-
248
- # image_url = None
249
- # if image is not None:
250
- # allowed = {"image/png", "image/jpeg", "image/jpg", "image/webp"}
251
- # if image.content_type not in allowed:
252
- # raise HTTPException(status_code=415, detail="Unsupported image type")
253
- # ext = {
254
- # "image/png": ".png",
255
- # "image/jpeg": ".jpg",
256
- # "image/jpg": ".jpg",
257
- # "image/webp": ".webp",
258
- # }[image.content_type]
259
- # filename = f"{uuid.uuid4().hex}{ext}"
260
- # path = os.path.join(UPLOAD_DIR, filename)
261
- # with open(path, "wb") as f:
262
- # f.write(await image.read())
263
- # image_url = f"/uploads/{filename}"
264
-
265
- # post = PostDB(user_id=user_id, content=content, image_url=image_url)
266
- # db.add(post)
267
- # db.commit()
268
- # db.refresh(post)
269
-
270
- # # make sure related fields exist for the response model
271
- # _ = post.user # author
272
- # post.comments = [] # new post → no comments yet
273
- # post.likes = [] # new post → no likes yet
274
-
275
- # # attach computed fields so Pydantic can serialize them
276
- # post.like_count = 0
277
- # post.is_liked_by_user = None # or False if you want the author to “not liked yet”
278
-
279
- # return post
280
-
281
- # @app.get("/users/{user_id}/posts", response_model=List[PostWithComments])
282
- # def list_user_posts(user_id: int, limit: int | None = None, db: Session = Depends(get_db)):
283
- # q = (db.query(PostDB)
284
- # .options(selectinload(PostDB.comments))
285
- # .filter(PostDB.user_id == user_id)
286
- # .order_by(PostDB.created_at.desc()))
287
- # if limit is not None:
288
- # q = q.limit(limit)
289
- # return q.all()
290
-
291
-
292
-
293
- # # Create a comment
294
- # @app.post("/comments", response_model=CommentOut, status_code=201)
295
- # def create_comment(payload: CommentCreate, db: Session = Depends(get_db)):
296
- # # Validate foreign keys up front (gives 404 instead of DB 500)
297
- # post = db.get(PostDB, payload.post_id)
298
- # if not post:
299
- # raise HTTPException(status_code=404, detail="Post not found")
300
- # user = db.get(UserDB, payload.user_id)
301
- # if not user:
302
- # raise HTTPException(status_code=404, detail="User not found")
303
-
304
- # # Create the comment
305
- # comment = CommentDB(
306
- # content=payload.content,
307
- # post_id=payload.post_id,
308
- # user_id=payload.user_id,
309
- # )
310
- # db.add(comment)
311
- # db.commit()
312
- # db.refresh(comment)
313
-
314
- # # Ensure the `user` relation is present on the returned object
315
- # # (either touch it to lazy-load, or eager-load with a second query)
316
- # _ = comment.user # touch to populate
317
- # return comment
318
-
319
- # # Single post with comments
320
- # @app.get("/posts/{post_id}", response_model=PostWithComments)
321
- # def get_post(post_id: int, db: Session = Depends(get_db)):
322
- # post = (
323
- # db.query(PostDB)
324
- # .options(selectinload(PostDB.comments)) # eager load comments
325
- # .filter(PostDB.id == post_id)
326
- # .first()
327
- # )
328
- # if not post:
329
- # raise HTTPException(status_code=404, detail="Post not found")
330
- # # Optionally order comments newest-first in memory:
331
- # post.comments.sort(key=lambda c: c.created_at, reverse=True)
332
- # return post
333
-
334
- # # List posts (for a specific user) with comments
335
-
336
- # @app.get("/posts", response_model=list[PostWithComments])
337
- # def list_posts(limit: int | None = None, viewer_id: int | None = None, db: Session = Depends(get_db)):
338
- # q = (
339
- # db.query(PostDB)
340
- # .options(
341
- # selectinload(PostDB.user),
342
- # selectinload(PostDB.comments).selectinload(CommentDB.user),
343
- # selectinload(PostDB.likes),
344
- # )
345
- # .order_by(PostDB.created_at.desc())
346
- # )
347
- # if limit: q = q.limit(limit)
348
- # posts = q.all()
349
-
350
- # liked_set = set()
351
- # if viewer_id:
352
- # rows = (
353
- # db.query(LikeDB.post_id)
354
- # .filter(LikeDB.user_id == viewer_id,
355
- # LikeDB.post_id.in_([p.id for p in posts]))
356
- # .all()
357
- # )
358
- # liked_set = {pid for (pid,) in rows}
359
-
360
- # for p in posts:
361
- # p.comments.sort(key=lambda c: c.created_at or datetime.min, reverse=True)
362
- # p.like_count = len(p.likes)
363
- # p.is_liked_by_user = (p.id in liked_set) if viewer_id else None
364
-
365
- # return posts
366
-
367
-
368
- # @app.post("/posts/{post_id}/like", response_model=PostWithComments)
369
- # def like_toggle(post_id: int, req: LikeRequest, db: Session = Depends(get_db)):
370
- # # Validate FK
371
- # user = db.get(UserDB, req.user_id)
372
- # if not user:
373
- # raise HTTPException(status_code=404, detail="User not found")
374
- # post = db.get(PostDB, post_id)
375
- # if not post:
376
- # raise HTTPException(status_code=404, detail="Post not found")
377
-
378
- # # Toggle
379
- # existing = db.query(LikeDB).filter_by(user_id=req.user_id, post_id=post_id).first()
380
- # if existing:
381
- # db.delete(existing)
382
- # db.commit()
383
- # # return updated full post object
384
- # return load_post_full(db, post_id, viewer_id=req.user_id)
385
-
386
- # try:
387
- # db.add(LikeDB(user_id=req.user_id, post_id=post_id))
388
- # db.commit()
389
- # except IntegrityError:
390
- # # in case of race: already liked; treat as unlike or just load
391
- # db.rollback()
392
- # return load_post_full(db, post_id, viewer_id=req.user_id)
393
-
394
-
395
- # def load_post_full(db: Session, post_id: int, viewer_id: int | None = None) -> PostDB:
396
- # post = (
397
- # db.query(PostDB)
398
- # .options(
399
- # selectinload(PostDB.user), # author
400
- # selectinload(PostDB.comments).selectinload(CommentDB.user), # commenters
401
- # selectinload(PostDB.likes), # likes for count
402
- # )
403
- # .filter(PostDB.id == post_id)
404
- # .first()
405
- # )
406
- # if not post:
407
- # raise HTTPException(status_code=404, detail="Post not found")
408
-
409
- # # compute like_count
410
- # post.like_count = len(post.likes)
411
-
412
- # # compute viewer flag (optional)
413
- # if viewer_id is not None:
414
- # liked = (
415
- # db.query(LikeDB)
416
- # .filter(LikeDB.user_id == viewer_id, LikeDB.post_id == post_id)
417
- # .first()
418
- # )
419
- # post.is_liked_by_user = liked is not None
420
- # else:
421
- # post.is_liked_by_user = None
422
-
423
- # # newest-first comments
424
- # post.comments.sort(key=lambda c: c.created_at or datetime.min, reverse=True)
425
- # return post
 
10
  from app.routers import expense
11
  from app.routers import categorybudget
12
  from app.routers import budget
13
+ # from openai import OpenAI
14
+ from app.routers import chat_ws
15
  # create missing tables (won't alter existing columns)
16
  Base.metadata.create_all(bind=engine)
17
 
 
43
  app.include_router(expense.router)
44
  app.include_router(categorybudget.router)
45
  app.include_router(budget.router)
46
+ app.include_router(chat_ws.router)
47
  # app.include_router(users.router)
48
  # app.include_router(posts.router)
49
  # app.include_router(comments.router)
50
  # app.include_router(likes.router)
51
 
52