GuglielmoTor commited on
Commit
3f4c92b
·
verified ·
1 Parent(s): c58585a

Update apis/Bubble_API_Calls.py

Browse files
Files changed (1) hide show
  1. apis/Bubble_API_Calls.py +78 -256
apis/Bubble_API_Calls.py CHANGED
@@ -1,310 +1,132 @@
1
- # bubble_api_calls.py
 
 
 
 
 
2
  import os
3
  import json
4
  import requests
5
  import pandas as pd
6
  import logging
7
 
 
 
 
8
  logger = logging.getLogger(__name__)
9
 
10
- def fetch_linkedin_token_from_bubble(url_user_token_str: str):
11
  """
12
- Fetches LinkedIn access token from Bubble.io API using the state value (url_user_token_str).
13
- The token is expected in a 'Raw_text' field as a JSON string, which is then parsed.
14
-
15
- Args:
16
- url_user_token_str: The state value (token from URL) to query Bubble.
17
-
18
- Returns:
19
- tuple: (parsed_token_dict, status_message)
20
- parsed_token_dict is the dictionary containing the token (e.g., {"access_token": "value"})
21
- or None if an error occurred or token not found.
22
- status_message is a string describing the outcome of the API call.
23
  """
24
- bubble_api_key = os.environ.get("Bubble_API")
25
- if not bubble_api_key:
26
- error_msg = "❌ Bubble API Error: The 'Bubble_API' environment variable is not set."
27
- print(error_msg)
28
- return None, error_msg
 
29
 
30
  if not url_user_token_str or "not found" in url_user_token_str or "Could not access" in url_user_token_str:
31
- status_msg = f"ℹ️ No valid user token from URL to query Bubble. ({url_user_token_str})"
32
- print(status_msg)
33
- return None, status_msg
34
 
35
- base_url = "https://app.ingaze.ai/version-test/api/1.1/obj/Linkedin_access"
36
  constraints = [{"key": "state", "constraint_type": "equals", "value": url_user_token_str}]
37
  params = {'constraints': json.dumps(constraints)}
38
  headers = {"Authorization": f"Bearer {bubble_api_key}"}
39
-
40
- status_message = f"Attempting to fetch token from Bubble for state: {url_user_token_str}..."
41
- print(status_message)
42
- parsed_token_dict = None
43
- response = None
44
 
 
45
  try:
46
  response = requests.get(base_url, params=params, headers=headers, timeout=15)
47
  response.raise_for_status()
48
-
49
  data = response.json()
50
  results = data.get("response", {}).get("results", [])
51
-
52
- if results:
53
- raw_text_from_bubble = results[0].get("Raw_text", None)
54
 
55
- if raw_text_from_bubble and isinstance(raw_text_from_bubble, str):
56
- try:
57
- temp_parsed_dict = json.loads(raw_text_from_bubble)
58
- if isinstance(temp_parsed_dict, dict) and "access_token" in temp_parsed_dict:
59
- parsed_token_dict = temp_parsed_dict # Successfully parsed and has access_token
60
- status_message = f"✅ LinkedIn Token successfully fetched and parsed from Bubble 'Raw_text' for state: {url_user_token_str}"
61
- elif isinstance(temp_parsed_dict, dict):
62
- status_message = (f"⚠️ Bubble API: 'access_token' key missing in parsed 'Raw_text' dictionary for state: {url_user_token_str}. Parsed: {temp_parsed_dict}")
63
- else: # Not a dict
64
- status_message = (f"⚠️ Bubble API: 'Raw_text' field did not contain a valid JSON dictionary string. "
65
- f"Content type: {type(raw_text_from_bubble)}, Value: {raw_text_from_bubble}")
66
- except json.JSONDecodeError as e:
67
- status_message = (f"⚠️ Bubble API: Error decoding 'Raw_text' JSON string: {e}. "
68
- f"Content: {raw_text_from_bubble}")
69
- elif raw_text_from_bubble: # It exists but is not a string
70
- status_message = (f"⚠️ Bubble API: 'Raw_text' field was not a string. "
71
- f"Type: {type(raw_text_from_bubble)}, Value: {raw_text_from_bubble}")
72
- else: # Raw_text not found or null
73
- status_message = (f"⚠️ Bubble API: Token field ('Raw_text') "
74
- f"not found or is null in response for state: {url_user_token_str}. Result: {results[0]}")
75
- else: # No results from Bubble for the given state
76
- status_message = f"❌ Bubble API: No results found for state: {url_user_token_str}"
77
-
78
- except requests.exceptions.HTTPError as http_err:
79
- error_details = response.text if response else "No response content"
80
- status_message = f"❌ Bubble API HTTP error: {http_err} - Response: {error_details}"
81
- except requests.exceptions.Timeout:
82
- status_message = "❌ Bubble API Request timed out."
83
- except requests.exceptions.RequestException as req_err:
84
- status_message = f"❌ Bubble API Request error: {req_err}"
85
- except json.JSONDecodeError as json_err: # Error decoding the main Bubble response
86
- error_details = response.text if response else "No response content"
87
- status_message = f"❌ Bubble API main response JSON decode error: {json_err}. Response: {error_details}"
88
  except Exception as e:
89
- status_message = f"An unexpected error occurred while fetching from Bubble: {str(e)}"
90
-
91
- print(status_message) # Log the final status message
92
- return parsed_token_dict
93
 
94
  def fetch_linkedin_posts_data_from_bubble(
95
- constraint_value: str,
96
- data_type: str,
97
- constraint_key: str,
98
- constraint_type: str,
99
  additional_constraints: list = None
100
- ):
101
  """
102
  Fetches data from the Bubble.io Data API, handling pagination to retrieve all results.
103
-
104
- Args:
105
- constraint_value: The value to match in the constraint.
106
- data_type: The Bubble data type (table name) to query.
107
- constraint_key: The field key for the constraint.
108
- constraint_type: The type of constraint (e.g., 'equals', 'contains').
109
- additional_constraints: A list of additional constraint dictionaries.
110
-
111
- Returns:
112
- A tuple containing a pandas DataFrame with all results and an error message string.
113
- If successful, the error message is None.
114
- If an error occurs, the DataFrame is None.
115
  """
116
- bubble_api_key = os.environ.get("Bubble_API")
117
- if not bubble_api_key:
118
- error_msg = "❌ Bubble API Error: The 'Bubble_API' environment variable is not set."
119
- print(error_msg)
 
 
120
  return None, error_msg
121
 
122
- base_url = f"https://app.ingaze.ai/version-test/api/1.1/obj/{data_type}"
123
  headers = {"Authorization": f"Bearer {bubble_api_key}"}
124
-
125
- # --- Main Constraint Setup ---
126
  constraints = [{"key": constraint_key, "constraint_type": constraint_type, "value": constraint_value}]
127
  if additional_constraints:
128
  constraints.extend(additional_constraints)
129
 
130
- # --- Pagination Logic ---
131
  all_results = []
132
- cursor = 0 # Start at the beginning
133
-
134
- print(f"Attempting to fetch data from Bubble for {constraint_key} = {constraint_value}...")
135
 
136
- while True: # Loop until all pages are fetched
137
- # Parameters for the API call, including constraints and pagination info
138
- params = {
139
- 'constraints': json.dumps(constraints),
140
- 'cursor': cursor,
141
- 'limit': 100 # Fetch up to 100 items per request (the max)
142
- }
143
-
144
  try:
145
-
146
- print(f"DEBUG: Requesting URL: {base_url}")
147
- print(f"DEBUG: Request PARAMS: {params}")
148
- # --- Make the API Request ---
149
  response = requests.get(base_url, params=params, headers=headers, timeout=30)
150
- response.raise_for_status() # Raises an HTTPError for bad responses (4xx or 5xx)
151
-
152
  data = response.json().get("response", {})
153
  results_on_page = data.get("results", [])
154
-
155
  if results_on_page:
156
  all_results.extend(results_on_page)
157
- print(f"Retrieved {len(results_on_page)} results on this page. Total so far: {len(all_results)}.")
158
-
159
- # --- Check if there are more pages ---
160
- # The API returns the number of results remaining. If 0, we're done.
161
  remaining = data.get("remaining", 0)
162
  if remaining == 0:
163
- print("No more pages to fetch.")
164
- break # Exit the while loop
165
-
166
- # --- Update cursor for the next page ---
167
- # The new cursor is the current cursor + number of items just received.
168
  cursor += len(results_on_page)
169
 
170
  except requests.exceptions.RequestException as e:
171
- error_msg = f"Bubble API Error: {str(e)}"
172
- print(error_msg)
 
 
 
 
173
  return None, error_msg
174
 
175
- # --- Final Processing ---
176
- if all_results:
177
- df = pd.DataFrame(all_results)
178
- print(f"✅ Successfully retrieved a total of {len(df)} posts.")
179
- return df, None
180
- else:
181
- print("No posts found for the given constraints.")
182
- # Return an empty DataFrame if nothing was found
183
  return pd.DataFrame(), None
184
 
185
-
186
- # def bulk_upload_to_bubble(data, data_type):
187
- # api_token = os.environ.get("Bubble_API")
188
- # url = f"https://app.ingaze.ai/version-test/api/1.1/obj/{data_type}/bulk"
189
- # headers = {
190
- # "Authorization": f"Bearer {api_token}",
191
- # "Content-Type": "text/plain"
192
- # }
193
-
194
- # # Convert list of dicts to newline-separated JSON strings
195
- # payload = "\n".join(json.dumps(item) for item in data)
196
- # response = requests.post(url, headers=headers, data=payload)
197
-
198
- # print("Payload being sent:")
199
- # print(payload)
200
-
201
- # if response.status_code == 200:
202
- # print(f"Successfully uploaded {len(data)} records to {data_type}.")
203
- # else:
204
- # print(f"Failed to upload data to {data_type}. Status Code: {response.status_code}, Response: {response.text}")
205
-
206
- #versione f49ffdd ultima che funzionava per upload dati linkedin
207
- def bulk_upload_to_bubble(data, data_type):
208
- """
209
- Uploads a list of dictionaries to a specified Bubble data type using the bulk endpoint.
210
- Args:
211
- data (list): A list of dictionaries, where each dictionary represents a record.
212
- data_type (str): The name of the Bubble data type (table) to upload to.
213
- Returns:
214
- list: A list of dictionaries (each containing an 'id') for the created records if successful.
215
- bool: False if the upload fails.
216
- """
217
- api_token = os.environ.get("Bubble_API")
218
- if not api_token:
219
- logger.error("Bubble_API environment variable not set.")
220
- return False
221
-
222
- url = f"https://app.ingaze.ai/version-test/api/1.1/obj/{data_type}/bulk"
223
- headers = {
224
- "Authorization": f"Bearer {api_token}",
225
- "Content-Type": "text/plain"
226
- }
227
- payload = "\n".join(json.dumps(item) for item in data)
228
- logging.info(f"Sending bulk payload to Bubble data type: {data_type}")
229
-
230
- try:
231
- response = requests.post(url, headers=headers, data=payload.encode('utf-8'))
232
- response.raise_for_status()
233
-
234
- # FIX: Handle the newline-delimited JSON response from Bubble.
235
- response_text = response.text.strip()
236
- if not response_text:
237
- logger.warning(f"Upload to {data_type} was successful but returned an empty response.")
238
- return [] # Return an empty list for success with no content
239
-
240
- created_records = []
241
- for line in response_text.splitlines():
242
- if line: # Ensure the line is not empty
243
- created_records.append(json.loads(line))
244
-
245
- logging.info(f"Successfully uploaded {len(created_records)} records to {data_type}.")
246
- return created_records
247
-
248
- except requests.exceptions.HTTPError as http_err:
249
- logger.error(f"HTTP error occurred: {http_err}")
250
- logger.error(f"Failed to upload data to {data_type}. Status Code: {response.status_code}, Response: {response.text}")
251
- return False
252
- except json.JSONDecodeError as json_err:
253
- # This error is what you were seeing. We log it in case the format changes again.
254
- logger.error(f"JSON decoding failed: {json_err}. Response text: {response.text}")
255
- return False
256
- except Exception as err:
257
- logger.error(f"An other error occurred: {err}", exc_info=True)
258
- return False
259
-
260
- def update_record_in_bubble(table_name, record_id, payload_to_update):
261
- """
262
- Updates an existing record in a Bubble.io table using a PATCH request.
263
-
264
- Args:
265
- table_name (str): The name of the Bubble table (e.g., "User", "Product").
266
- record_id (str): The unique ID of the record to update.
267
- payload_to_update (dict): A dictionary where keys are field names (slugs)
268
- and values are the new values for those fields.
269
- Returns:
270
- bool: True if the update was successful, False otherwise.
271
- """
272
- bubble_api_key = os.environ.get("Bubble_API")
273
- if not record_id:
274
- logging.error(f"Record ID is missing. Cannot update record in Bubble table '{table_name}'.")
275
- return False
276
- if not payload_to_update:
277
- logging.warning(f"Payload to update is empty for record_id '{record_id}' in Bubble table '{table_name}'. Nothing to update.")
278
- # Consider this a success as there's no error, just no action.
279
- # Depending on desired behavior, you might return False or raise an error.
280
- return True
281
-
282
- # Construct the API endpoint for a specific record
283
- # Example: https://<app_name>.bubbleapps.io/api/1.1/obj/<table_name>/<record_id>
284
- api_endpoint = f"https://app.ingaze.ai/version-test/api/1.1/obj/{table_name}/{record_id}"
285
- headers = {
286
- "Authorization": f"Bearer {bubble_api_key}",
287
- "Content-Type": "application/json"
288
- }
289
-
290
- logging.debug(f"Attempting to update record '{record_id}' in table '{table_name}' at endpoint '{api_endpoint}' with payload: {payload_to_update}")
291
-
292
- try:
293
- response = requests.patch(api_endpoint, json=payload_to_update, headers=headers)
294
- response.raise_for_status() # Raises an HTTPError for bad responses (4XX or 5XX)
295
- logging.info(f"Successfully updated record '{record_id}' in Bubble table '{table_name}'.")
296
- return True
297
- except requests.exceptions.HTTPError as http_err:
298
- # Log more details from the response if available
299
- error_details = ""
300
- try:
301
- error_details = response.json() # Bubble often returns JSON errors
302
- except ValueError: # If response is not JSON
303
- error_details = response.text
304
- logging.error(f"HTTP error occurred while updating record '{record_id}' in '{table_name}': {http_err}. Response: {error_details}")
305
- except requests.exceptions.RequestException as req_err:
306
- logging.error(f"Request exception occurred while updating record '{record_id}' in '{table_name}': {req_err}")
307
- except Exception as e:
308
- logging.error(f"An unexpected error occurred while updating record '{record_id}' in '{table_name}': {e}", exc_info=True)
309
-
310
- return False
 
1
+ # apis/Bubble_API_Calls.py
2
+ """
3
+ This module provides functions to read data from the Bubble.io API.
4
+ It is used to fetch the LinkedIn token and all pre-processed application data.
5
+ All data writing/updating functions have been removed.
6
+ """
7
  import os
8
  import json
9
  import requests
10
  import pandas as pd
11
  import logging
12
 
13
+ from config import BUBBLE_API_KEY_PRIVATE_ENV_VAR, BUBBLE_API_ENDPOINT_ENV_VAR, BUBBLE_APP_NAME_ENV_VAR
14
+
15
+ logging.basicConfig(level=logging.INFO)
16
  logger = logging.getLogger(__name__)
17
 
18
+ def fetch_linkedin_token_from_bubble(url_user_token_str: str) -> Optional[dict]:
19
  """
20
+ Fetches LinkedIn access token from Bubble.io using a state value.
21
+ The token is expected in a 'Raw_text' field as a JSON string.
 
 
 
 
 
 
 
 
 
22
  """
23
+ bubble_api_key = os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR)
24
+ app_name = os.environ.get(BUBBLE_APP_NAME_ENV_VAR)
25
+
26
+ if not bubble_api_key or not app_name:
27
+ logger.error("Bubble API environment variables (key or app name) are not set.")
28
+ return None
29
 
30
  if not url_user_token_str or "not found" in url_user_token_str or "Could not access" in url_user_token_str:
31
+ logger.info(f"No valid user token provided to query Bubble: {url_user_token_str}")
32
+ return None
 
33
 
34
+ base_url = f"https://{app_name}.bubbleapps.io/version-test/api/1.1/obj/Linkedin_access"
35
  constraints = [{"key": "state", "constraint_type": "equals", "value": url_user_token_str}]
36
  params = {'constraints': json.dumps(constraints)}
37
  headers = {"Authorization": f"Bearer {bubble_api_key}"}
 
 
 
 
 
38
 
39
+ logger.info(f"Attempting to fetch LinkedIn token from Bubble for state: {url_user_token_str}")
40
  try:
41
  response = requests.get(base_url, params=params, headers=headers, timeout=15)
42
  response.raise_for_status()
 
43
  data = response.json()
44
  results = data.get("response", {}).get("results", [])
 
 
 
45
 
46
+ if not results:
47
+ logger.warning(f"No token results found in Bubble for state: {url_user_token_str}")
48
+ return None
49
+
50
+ raw_text = results[0].get("Raw_text")
51
+ if not raw_text or not isinstance(raw_text, str):
52
+ logger.warning(f"Token 'Raw_text' field is missing or not a string. Value: {raw_text}")
53
+ return None
54
+
55
+ parsed_token = json.loads(raw_text)
56
+ if isinstance(parsed_token, dict) and "access_token" in parsed_token:
57
+ logger.info("Successfully fetched and parsed LinkedIn token from Bubble.")
58
+ return parsed_token
59
+ else:
60
+ logger.error(f"Parsed token from Bubble is not a valid dictionary with an access_token. Parsed value: {parsed_token}")
61
+ return None
62
+
63
+ except requests.exceptions.RequestException as e:
64
+ logger.error(f"Bubble API request error while fetching token: {e}", exc_info=True)
65
+ except json.JSONDecodeError as e:
66
+ logger.error(f"Error decoding JSON from Bubble token response: {e}", exc_info=True)
 
 
 
 
 
 
 
 
 
 
 
 
67
  except Exception as e:
68
+ logger.error(f"An unexpected error occurred while fetching the token from Bubble: {e}", exc_info=True)
69
+
70
+ return None
71
+
72
 
73
  def fetch_linkedin_posts_data_from_bubble(
74
+ data_type: str,
75
+ constraint_key: str,
76
+ constraint_type: str,
77
+ constraint_value: any,
78
  additional_constraints: list = None
79
+ ) -> Tuple[Optional[pd.DataFrame], Optional[str]]:
80
  """
81
  Fetches data from the Bubble.io Data API, handling pagination to retrieve all results.
 
 
 
 
 
 
 
 
 
 
 
 
82
  """
83
+ bubble_api_key = os.environ.get(BUBBLE_API_KEY_PRIVATE_ENV_VAR)
84
+ api_endpoint = os.environ.get(BUBBLE_API_ENDPOINT_ENV_VAR)
85
+
86
+ if not bubble_api_key or not api_endpoint:
87
+ error_msg = "Bubble API environment variables (key or endpoint) are not set."
88
+ logger.error(error_msg)
89
  return None, error_msg
90
 
91
+ base_url = f"{api_endpoint}/{data_type}"
92
  headers = {"Authorization": f"Bearer {bubble_api_key}"}
93
+
 
94
  constraints = [{"key": constraint_key, "constraint_type": constraint_type, "value": constraint_value}]
95
  if additional_constraints:
96
  constraints.extend(additional_constraints)
97
 
 
98
  all_results = []
99
+ cursor = 0
100
+ logger.info(f"Fetching data from Bubble type '{data_type}' where '{constraint_key}' is '{constraint_value}'...")
 
101
 
102
+ while True:
103
+ params = {'constraints': json.dumps(constraints), 'cursor': cursor, 'limit': 100}
 
 
 
 
 
 
104
  try:
 
 
 
 
105
  response = requests.get(base_url, params=params, headers=headers, timeout=30)
106
+ response.raise_for_status()
107
+
108
  data = response.json().get("response", {})
109
  results_on_page = data.get("results", [])
 
110
  if results_on_page:
111
  all_results.extend(results_on_page)
112
+
 
 
 
113
  remaining = data.get("remaining", 0)
114
  if remaining == 0:
115
+ break
 
 
 
 
116
  cursor += len(results_on_page)
117
 
118
  except requests.exceptions.RequestException as e:
119
+ error_msg = f"Bubble API Error fetching '{data_type}': {e}"
120
+ logger.error(error_msg, exc_info=True)
121
+ return None, error_msg
122
+ except json.JSONDecodeError as e:
123
+ error_msg = f"JSON Decode Error fetching '{data_type}': {e}. Response text: {response.text}"
124
+ logger.error(error_msg, exc_info=True)
125
  return None, error_msg
126
 
127
+ if not all_results:
128
+ logger.info(f"No data found in Bubble for the given constraints in data type '{data_type}'.")
 
 
 
 
 
 
129
  return pd.DataFrame(), None
130
 
131
+ logger.info(f"Successfully retrieved a total of {len(all_results)} records from '{data_type}'.")
132
+ return pd.DataFrame(all_results), None