Rafs-an09002 commited on
Commit
805aa65
Β·
verified Β·
1 Parent(s): 5f02c71

sync: backend from GitHub Actions

Browse files
Files changed (1) hide show
  1. services/firebase_service.py +59 -56
services/firebase_service.py CHANGED
@@ -1,6 +1,6 @@
1
  """
2
  Firebase Service - Handles persistence and logging using Firebase RTDB REST API.
3
- This is optimized for low-resource environments (Render.com Free Tier) and avoids heavy SDKs.
4
  """
5
 
6
  import requests
@@ -10,58 +10,55 @@ import json
10
  import os
11
  from datetime import datetime
12
 
13
- # Local Imports
14
- from backend.config import FIREBASE_CONFIG
15
 
16
  logger = logging.getLogger(__name__)
17
 
18
  class FirebaseService:
19
  """Singleton class for Firebase Realtime Database operations via REST API."""
20
-
21
  _instance = None
22
 
23
  def __new__(cls):
24
- """Implement Singleton pattern."""
25
  if cls._instance is None:
26
  cls._instance = super(FirebaseService, cls).__new__(cls)
27
-
28
- # CRITICAL FIX: Direct URL injection for guaranteed testing.
29
- # RENDER EV should be set, but this line ensures the correct URL is used.
30
  env_url = os.getenv('FIREBASE_DATABASE_URL', "https://default-rtdb.firebaseio.com")
31
-
32
- # Use the URL you provided
33
- cls._instance._base_url = "https://rating-system-c7adc-default-rtdb.asia-southeast1.firebasedatabase.app/" if 'default-rtdb.firebaseio.com' in env_url else env_url
34
-
35
- cls._instance._auth = FIREBASE_CONFIG['apiKey']
36
- cls._instance._session = requests.Session()
37
-
 
 
 
38
  if not cls._instance._base_url.endswith('/'):
39
- cls._instance._base_url += '/'
40
-
41
  if 'default-rtdb.firebaseio.com' in cls._instance._base_url:
42
  logger.error(
43
  "❌ CRITICAL FIREBASE URL ERROR: Persistence will FAIL. "
44
- "You MUST set FIREBASE_DATABASE_URL to your actual URL in Render EV."
45
  )
46
-
47
- logger.info(f"βœ… Firebase REST Service initialized with URL: {cls._instance._base_url}")
48
-
49
  return cls._instance
50
 
51
  def _send_request(self, method: str, path: str, data: Optional[Dict] = None) -> Optional[Dict]:
52
- """Generic method to send a request to Firebase RTDB."""
53
-
54
  if 'default-rtdb.firebaseio.com' in self._base_url:
55
  return None
56
-
57
- url = self._base_url + path + ".json"
58
  params = {}
59
  if self._auth:
60
  params['auth'] = self._auth
61
-
62
  try:
63
- timeout = 3 if method in ['PUT', 'POST'] else 5
64
-
65
  if method == 'PUT':
66
  response = self._session.put(url, params=params, json=data, timeout=timeout)
67
  elif method == 'GET':
@@ -72,59 +69,65 @@ class FirebaseService:
72
  response = self._session.delete(url, params=params, timeout=timeout)
73
  else:
74
  return None
75
-
76
  if response.status_code in [401, 404]:
77
- logger.error(f"❌ Firebase REST Error ({method} {path}): {response.status_code} {response.reason}. Check Rules or URL.")
78
-
79
  response.raise_for_status()
80
  return response.json()
81
-
82
  except requests.exceptions.RequestException as e:
83
  logger.error(f"❌ Firebase REST Error ({method} {path}): {e}")
84
  return None
85
-
86
- # --- REST OF THE FUNCTIONS (UNCHANGED) ---
87
  def save_game_state(self, game_state):
88
  try:
89
- state_dict = game_state.to_dict()
90
- self._send_request('PUT', f'games/{game_state.session_id}', state_dict)
91
  except Exception:
92
  pass
93
 
94
  def load_game_state(self, session_id):
95
  return self._send_request('GET', f'games/{session_id}')
96
-
97
  def delete_game_state(self, session_id):
98
  self._send_request('DELETE', f'games/{session_id}')
99
 
100
- def log_game_result(self, game_state, final_guess, confidence, was_correct, failure_reason, actual_answer=None):
 
101
  result_data = {
102
- 'session_id': game_state.session_id,
103
- 'category': game_state.category,
104
- 'questions_asked': game_state.questions_asked,
105
- 'final_guess': final_guess,
106
- 'actual_answer': actual_answer,
107
- 'was_correct': was_correct,
108
- 'confidence': round(confidence, 1),
109
- 'failure_reason': failure_reason,
110
- 'duration_seconds': round(game_state.get_game_duration(), 2),
111
  'answer_history_summary': game_state.get_answer_statistics(),
112
- 'timestamp': datetime.now().isoformat()
113
  }
114
  self._send_request('POST', 'analytics/game_results', result_data)
115
-
116
  def update_question_effectiveness(self, question, information_gain, was_effective):
117
- attr = question['attribute']
118
- val_key = str(question['value']).replace('.', '_').replace('#', '_').replace('$', '_').replace('[', '_').replace(']', '_')
 
 
119
  category = question['category']
120
- path = f'learning/questions/{category}/{attr}/{val_key}'
 
121
  current_data = self._send_request('GET', path)
122
-
123
  if current_data is None:
124
- current_data = {'times_asked': 0, 'total_ig': 0.0, 'effective_count': 0, 'question_text': question['question']}
 
 
 
 
 
125
 
126
- current_data['times_asked'] += 1
127
- current_data['total_ig'] += information_gain
128
  if was_effective:
129
  current_data['effective_count'] += 1
130
  current_data['avg_ig'] = current_data['total_ig'] / current_data['times_asked']
 
1
  """
2
  Firebase Service - Handles persistence and logging using Firebase RTDB REST API.
3
+ Optimized for low-resource environments β€” uses lightweight REST API, no heavy SDKs.
4
  """
5
 
6
  import requests
 
10
  import os
11
  from datetime import datetime
12
 
13
+ from config import FIREBASE_CONFIG
 
14
 
15
  logger = logging.getLogger(__name__)
16
 
17
  class FirebaseService:
18
  """Singleton class for Firebase Realtime Database operations via REST API."""
19
+
20
  _instance = None
21
 
22
  def __new__(cls):
 
23
  if cls._instance is None:
24
  cls._instance = super(FirebaseService, cls).__new__(cls)
25
+
 
 
26
  env_url = os.getenv('FIREBASE_DATABASE_URL', "https://default-rtdb.firebaseio.com")
27
+
28
+ cls._instance._base_url = (
29
+ "https://rating-system-c7adc-default-rtdb.asia-southeast1.firebasedatabase.app/"
30
+ if 'default-rtdb.firebaseio.com' in env_url
31
+ else env_url
32
+ )
33
+
34
+ cls._instance._auth = FIREBASE_CONFIG['apiKey']
35
+ cls._instance._session = requests.Session()
36
+
37
  if not cls._instance._base_url.endswith('/'):
38
+ cls._instance._base_url += '/'
39
+
40
  if 'default-rtdb.firebaseio.com' in cls._instance._base_url:
41
  logger.error(
42
  "❌ CRITICAL FIREBASE URL ERROR: Persistence will FAIL. "
43
+ "Set FIREBASE_DATABASE_URL in HF Space secrets."
44
  )
45
+
46
+ logger.info(f"βœ… Firebase REST Service initialized: {cls._instance._base_url}")
47
+
48
  return cls._instance
49
 
50
  def _send_request(self, method: str, path: str, data: Optional[Dict] = None) -> Optional[Dict]:
 
 
51
  if 'default-rtdb.firebaseio.com' in self._base_url:
52
  return None
53
+
54
+ url = self._base_url + path + ".json"
55
  params = {}
56
  if self._auth:
57
  params['auth'] = self._auth
58
+
59
  try:
60
+ timeout = 3 if method in ['PUT', 'POST'] else 5
61
+
62
  if method == 'PUT':
63
  response = self._session.put(url, params=params, json=data, timeout=timeout)
64
  elif method == 'GET':
 
69
  response = self._session.delete(url, params=params, timeout=timeout)
70
  else:
71
  return None
72
+
73
  if response.status_code in [401, 404]:
74
+ logger.error(f"❌ Firebase REST Error ({method} {path}): {response.status_code}")
75
+
76
  response.raise_for_status()
77
  return response.json()
78
+
79
  except requests.exceptions.RequestException as e:
80
  logger.error(f"❌ Firebase REST Error ({method} {path}): {e}")
81
  return None
82
+
 
83
  def save_game_state(self, game_state):
84
  try:
85
+ self._send_request('PUT', f'games/{game_state.session_id}', game_state.to_dict())
 
86
  except Exception:
87
  pass
88
 
89
  def load_game_state(self, session_id):
90
  return self._send_request('GET', f'games/{session_id}')
91
+
92
  def delete_game_state(self, session_id):
93
  self._send_request('DELETE', f'games/{session_id}')
94
 
95
+ def log_game_result(self, game_state, final_guess, confidence,
96
+ was_correct, failure_reason, actual_answer=None):
97
  result_data = {
98
+ 'session_id': game_state.session_id,
99
+ 'category': game_state.category,
100
+ 'questions_asked': game_state.questions_asked,
101
+ 'final_guess': final_guess,
102
+ 'actual_answer': actual_answer,
103
+ 'was_correct': was_correct,
104
+ 'confidence': round(confidence, 1),
105
+ 'failure_reason': failure_reason,
106
+ 'duration_seconds': round(game_state.get_game_duration(), 2),
107
  'answer_history_summary': game_state.get_answer_statistics(),
108
+ 'timestamp': datetime.now().isoformat()
109
  }
110
  self._send_request('POST', 'analytics/game_results', result_data)
111
+
112
  def update_question_effectiveness(self, question, information_gain, was_effective):
113
+ attr = question['attribute']
114
+ val_key = (str(question['value'])
115
+ .replace('.', '_').replace('#', '_')
116
+ .replace('$', '_').replace('[', '_').replace(']', '_'))
117
  category = question['category']
118
+ path = f'learning/questions/{category}/{attr}/{val_key}'
119
+
120
  current_data = self._send_request('GET', path)
 
121
  if current_data is None:
122
+ current_data = {
123
+ 'times_asked': 0,
124
+ 'total_ig': 0.0,
125
+ 'effective_count': 0,
126
+ 'question_text': question['question']
127
+ }
128
 
129
+ current_data['times_asked'] += 1
130
+ current_data['total_ig'] += information_gain
131
  if was_effective:
132
  current_data['effective_count'] += 1
133
  current_data['avg_ig'] = current_data['total_ig'] / current_data['times_asked']