internationalscholarsprogram commited on
Commit
158fe8d
·
1 Parent(s): f4df84c

chore: remove unused database/SQLAlchemy dependency

Browse files

App only uses JSON APIs - no database needed.
Removes sqlalchemy, pymysql, database.py, handbook_repo.py.
Reduces Docker image size and eliminates DB connection failures.

app/core/config.py CHANGED
@@ -14,14 +14,6 @@ class Settings(BaseSettings):
14
  debug: bool = False
15
  port: int = 7860 # Hugging Face Spaces default
16
 
17
- # Database
18
- db_host: str = "localhost"
19
- db_port: int = 3306
20
- db_user: str = "root"
21
- db_password: str = ""
22
- db_name: str = "handbook"
23
- db_charset: str = "utf8mb4"
24
-
25
  # External API endpoints (the source-of-truth JSON APIs)
26
  handbook_general_endpoint: str = ""
27
  university_handbook_endpoint: str = ""
@@ -43,14 +35,6 @@ class Settings(BaseSettings):
43
 
44
  model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
45
 
46
- @property
47
- def database_url(self) -> str:
48
- return (
49
- f"mysql+pymysql://{self.db_user}:{self.db_password}"
50
- f"@{self.db_host}:{self.db_port}/{self.db_name}"
51
- f"?charset={self.db_charset}"
52
- )
53
-
54
  @property
55
  def cors_origins_list(self) -> list[str]:
56
  return [o.strip() for o in self.cors_origins.split(",") if o.strip()]
 
14
  debug: bool = False
15
  port: int = 7860 # Hugging Face Spaces default
16
 
 
 
 
 
 
 
 
 
17
  # External API endpoints (the source-of-truth JSON APIs)
18
  handbook_general_endpoint: str = ""
19
  university_handbook_endpoint: str = ""
 
35
 
36
  model_config = {"env_file": ".env", "env_file_encoding": "utf-8", "extra": "ignore"}
37
 
 
 
 
 
 
 
 
 
38
  @property
39
  def cors_origins_list(self) -> list[str]:
40
  return [o.strip() for o in self.cors_origins.split(",") if o.strip()]
app/core/database.py DELETED
@@ -1,48 +0,0 @@
1
- """Database engine and session factory (SQLAlchemy).
2
-
3
- Engine is created lazily to avoid crash on startup when no DB is available
4
- (e.g. Hugging Face Spaces without MySQL).
5
- """
6
-
7
- import logging
8
- from sqlalchemy import create_engine
9
- from sqlalchemy.orm import sessionmaker, DeclarativeBase
10
-
11
- from app.core.config import get_settings
12
-
13
- logger = logging.getLogger(__name__)
14
-
15
-
16
- class Base(DeclarativeBase):
17
- pass
18
-
19
-
20
- _engine = None
21
-
22
-
23
- def _get_engine():
24
- global _engine
25
- if _engine is None:
26
- _settings = get_settings()
27
- try:
28
- _engine = create_engine(
29
- _settings.database_url,
30
- pool_pre_ping=True,
31
- pool_recycle=3600,
32
- echo=_settings.debug,
33
- )
34
- except Exception as exc:
35
- logger.warning("Database engine creation failed: %s", exc)
36
- raise
37
- return _engine
38
-
39
-
40
- def get_db():
41
- """FastAPI dependency: yields a DB session, closes on exit."""
42
- engine = _get_engine()
43
- Session = sessionmaker(autocommit=False, autoflush=False, bind=engine)
44
- db = Session()
45
- try:
46
- yield db
47
- finally:
48
- db.close()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/repositories/handbook_repo.py DELETED
@@ -1,124 +0,0 @@
1
- """Repository layer — direct DB access if needed.
2
-
3
- The PHP code primarily fetches data over HTTP from two JSON APIs rather than
4
- querying the database directly. This repository provides a DB-backed fallback
5
- for when the Python service needs to read from MySQL directly (e.g., if the
6
- external endpoints are unreachable or the service is meant to replace them).
7
-
8
- ASSUMPTION: The database tables match the JSON structures returned by the PHP
9
- endpoints. Table/column names are preserved exactly.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import json
15
- import logging
16
- from typing import Any
17
-
18
- from sqlalchemy import text
19
- from sqlalchemy.orm import Session
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
-
24
- class HandbookRepository:
25
- """Read-only queries against the existing MySQL schema."""
26
-
27
- def __init__(self, db: Session):
28
- self.db = db
29
-
30
- # ── Global / general sections ──
31
-
32
- def get_general_sections(self, catalog_id: int = 0) -> list[dict[str, Any]]:
33
- """Fetch handbook_general_sections rows.
34
-
35
- ASSUMPTION: Table is named `handbook_general_sections` with columns:
36
- id, section_key, section_title, section_json, sort_order, catalog_id.
37
- Adjust if the actual schema differs.
38
- """
39
- query = text(
40
- "SELECT id, section_key, section_title, section_json, sort_order "
41
- "FROM handbook_general_sections "
42
- "WHERE (:catalog_id = 0 OR catalog_id = :catalog_id) "
43
- "ORDER BY sort_order ASC, id ASC"
44
- )
45
- rows = self.db.execute(query, {"catalog_id": catalog_id}).mappings().all()
46
- results = []
47
- for row in rows:
48
- section_json = row.get("section_json", "{}")
49
- if isinstance(section_json, str):
50
- try:
51
- section_json = json.loads(section_json) if section_json.strip() else {}
52
- except (json.JSONDecodeError, ValueError):
53
- logger.warning(
54
- "Failed to parse section_json for id=%s key=%s",
55
- row.get("id"),
56
- row.get("section_key"),
57
- )
58
- section_json = {}
59
-
60
- results.append(
61
- {
62
- "id": row["id"],
63
- "section_key": row.get("section_key", ""),
64
- "section_title": row.get("section_title", ""),
65
- "section_json": section_json,
66
- "sort_order": row.get("sort_order"),
67
- }
68
- )
69
- return results
70
-
71
- # ── University sections ──
72
-
73
- def get_university_sections(self) -> list[dict[str, Any]]:
74
- """Fetch university data with their sections.
75
-
76
- ASSUMPTION: Tables `universities` and `university_handbook_sections` exist.
77
- Adjust table/column names to match the real schema.
78
- """
79
- uni_query = text(
80
- "SELECT id AS university_id, university_name, is_active, website "
81
- "FROM universities "
82
- "ORDER BY university_name ASC"
83
- )
84
- unis = self.db.execute(uni_query).mappings().all()
85
-
86
- results = []
87
- for uni in unis:
88
- uid = uni["university_id"]
89
- sec_query = text(
90
- "SELECT id, section_key, section_title, section_json "
91
- "FROM university_handbook_sections "
92
- "WHERE university_id = :uid "
93
- "ORDER BY sort_order ASC, id ASC"
94
- )
95
- sec_rows = self.db.execute(sec_query, {"uid": uid}).mappings().all()
96
-
97
- sections = []
98
- for s in sec_rows:
99
- sj = s.get("section_json", "{}")
100
- if isinstance(sj, str):
101
- try:
102
- sj = json.loads(sj) if sj.strip() else {}
103
- except (json.JSONDecodeError, ValueError):
104
- sj = {}
105
- sections.append(
106
- {
107
- "id": s["id"],
108
- "section_key": s.get("section_key", ""),
109
- "section_title": s.get("section_title", ""),
110
- "section_json": sj,
111
- }
112
- )
113
-
114
- results.append(
115
- {
116
- "university_id": uid,
117
- "university_name": uni.get("university_name", ""),
118
- "is_active": bool(uni.get("is_active", True)),
119
- "website": uni.get("website", ""),
120
- "sections": sections,
121
- }
122
- )
123
-
124
- return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
requirements.txt CHANGED
@@ -2,8 +2,6 @@ fastapi>=0.110.0
2
  uvicorn[standard]>=0.27.0
3
  pydantic>=2.5.0
4
  pydantic-settings>=2.1.0
5
- sqlalchemy>=2.0.0
6
- pymysql>=1.1.0
7
  httpx>=0.27.0
8
  jinja2>=3.1.0
9
  markupsafe>=2.1.0
 
2
  uvicorn[standard]>=0.27.0
3
  pydantic>=2.5.0
4
  pydantic-settings>=2.1.0
 
 
5
  httpx>=0.27.0
6
  jinja2>=3.1.0
7
  markupsafe>=2.1.0