Coverage for mindsdb / integrations / handlers / instatus_handler / instatus_tables.py: 0%
183 statements
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-21 00:36 +0000
« prev ^ index » next coverage.py v7.13.1, created at 2026-01-21 00:36 +0000
1from typing import List
2import pandas as pd
3from mindsdb.integrations.libs.api_handler import APITable
4from mindsdb_sql_parser import ast
5from mindsdb.integrations.utilities.sql_utils import extract_comparison_conditions
6from mindsdb_sql_parser.ast.select.constant import Constant
7import json
8import re
10langCodes = ["ar", "cs", "da", "de", "en", "es", "et", "fi", "fr", "hu", "id", "it", "ja", "ko",
11 "nl", "no", "pl", "pt", "pt-BR", "ro", "rs", "ru", "sl", "sq", "sv", "tr", "uk",
12 "vi", "zh", "zh-TW"]
15class StatusPages(APITable):
17 # table name in the database
18 name = 'status_pages'
20 def select(self, query: ast.Select) -> pd.DataFrame:
21 """Receive query as AST (abstract syntax tree) and act upon it.
23 Args:
24 query (ASTNode): sql query represented as AST. Usually it should be ast.Select
26 Returns:
27 pd.DataFrame
28 """
29 conditions = extract_comparison_conditions(query.where)
30 # Get page id from query
31 _id = None
32 for op, arg1, arg2 in conditions:
33 if arg1 == 'id' and op == '=':
34 _id = arg2
35 else:
36 raise NotImplementedError
38 # Get column names from query
39 selected_columns = []
40 for target in query.targets:
41 if isinstance(target, ast.Star):
42 selected_columns = self.get_columns()
43 break
44 elif isinstance(target, ast.Identifier):
45 selected_columns.append(target.parts[-1])
46 else:
47 raise ValueError(f"Unknown query target {type(target)}")
49 # 'id' needs to selected when searching with 'id'
50 temp_selected_columns = selected_columns
51 if _id and 'id' not in selected_columns:
52 selected_columns = ['id'] + selected_columns
54 # Get limit from query
55 limit = query.limit.value if query.limit else 20
56 total_results = limit
58 page_no = 1 # default page no
59 result_df = pd.DataFrame(columns=selected_columns)
61 # call instatus api and get the response as pd.DataFrame
62 while True:
63 df = self.handler.call_instatus_api(endpoint='/v2/pages', params={'page': page_no, 'per_page': 100})
64 if len(df) == 0 or limit <= 0:
65 break
66 else:
67 result_df = pd.concat([result_df, df[selected_columns]], ignore_index=True)
69 page_no += 1
70 limit -= len(df)
72 # select columns from pandas data frame df
73 if result_df.empty:
74 result_df = pd.DataFrame(columns=selected_columns)
75 elif _id:
76 result_df = result_df[result_df['id'] == _id]
78 # delete 'id' column if 'id' not present in temp_selected_columns
79 if 'id' not in temp_selected_columns and 'id' in selected_columns:
80 result_df = result_df.drop('id', axis=1)
82 return result_df.head(n=total_results)
84 def insert(self, query: ast.Insert) -> None:
85 """Receive query as AST (abstract syntax tree) and act upon it somehow.
87 Args:
88 query (ASTNode): sql query represented as AST. Usually it should be ast.Insert
90 Returns:
91 None
92 """
93 data = {}
94 for column, value in zip(query.columns, query.values[0]):
95 if isinstance(value, str):
96 try:
97 value = json.loads(value)
98 except json.JSONDecodeError:
99 if value == 'True':
100 value = True
101 elif value == 'False':
102 value = False
103 data[column.name] = value
104 self.handler.call_instatus_api(endpoint='/v1/pages', method='POST', json_data=data)
106 def update(self, query: ast.Update) -> None:
107 """Receive query as AST (abstract syntax tree) and act upon it somehow.
109 Args:
110 query (ASTNode): sql query represented as AST. Usually it should be ast.Update
111 Returns:
112 None
113 """
114 conditions = extract_comparison_conditions(query.where)
115 # Get page id from query
116 _id = None
117 for op, arg1, arg2 in conditions:
118 if arg1 == 'id' and op == '=':
119 _id = arg2
120 else:
121 raise NotImplementedError
123 data = {}
124 for key, value in query.update_columns.items():
125 if isinstance(value, Constant):
126 if key == 'components':
127 data[key] = json.loads(value.value)
128 else:
129 data[key] = value.value
131 if 'components' in data and isinstance(data['components'], str):
132 data['components'] = json.loads(data['components'])
134 self.handler.call_instatus_api(endpoint=f'/v2/{_id}', method='PUT', json_data=data)
136 def get_columns(self, ignore: List[str] = []) -> List[str]:
137 """columns
139 Args:
140 ignore (List[str], optional): exclusion items. Defaults to [].
142 Returns:
143 List[str]: available columns with `ignore` items removed from the list.
144 """
145 return [
146 "id",
147 "subdomain",
148 "name",
149 "workspaceId",
150 "logoUrl",
151 "faviconUrl",
152 "websiteUrl",
153 "customDomain",
154 "publicEmail",
155 "twitter",
156 "status",
157 "subscribeBySms",
158 "sendSmsNotifications",
159 "language",
160 "useLargeHeader",
161 "brandColor",
162 "okColor",
163 "disruptedColor",
164 "degradedColor",
165 "downColor",
166 "noticeColor",
167 "unknownColor",
168 "googleAnalytics",
169 "smsService",
170 "htmlInMeta",
171 "htmlAboveHeader",
172 "htmlBelowHeader",
173 "htmlAboveFooter",
174 "htmlBelowFooter",
175 "htmlBelowSummary",
176 "uptimeDaysDisplay",
177 "uptimeOutageDisplay",
178 "launchDate",
179 "cssGlobal",
180 "onboarded",
181 "createdAt",
182 "updatedAt"
183 ]
186class Components(APITable):
188 # table name in the database
189 name = 'components'
191 def select(self, query: ast.Select) -> pd.DataFrame:
192 """Receive query as AST (abstract syntax tree) and act upon it.
194 Args:
195 query (ASTNode): SQL query represented as AST. Usually it should be ast.Select
197 Returns:
198 pd.DataFrame
199 """
200 conditions = extract_comparison_conditions(query.where)
202 if len(conditions) == 0:
203 raise Exception('WHERE clause is required')
205 # Get page id and component id from query
206 pageId = None
207 componentId = None
208 for condition in conditions:
209 if condition[1] == 'page_id' and condition[0] == '=':
210 pageId = condition[2]
212 if condition[1] == 'component_id' and condition[0] == '=':
213 componentId = condition[2]
215 # Get column names from query
216 selected_columns = []
217 for target in query.targets:
218 if isinstance(target, ast.Star):
219 selected_columns = self.get_columns()
220 break
221 elif isinstance(target, ast.Identifier):
222 selected_columns.append(target.parts[-1])
223 else:
224 raise ValueError(f"Unknown query target {type(target)}")
226 limit = query.limit.value if query.limit else None
227 if componentId:
228 # Call instatus API and get the response as pd.DataFrame
229 df = self.handler.call_instatus_api(endpoint=f'/v1/{pageId}/components/{componentId}')
230 for langCode in langCodes:
231 try:
232 df[f"translations_name_in_{langCode}"] = df["translations"].apply(lambda x: x.get("name", None)).apply(lambda x: x.get(langCode, None))
233 df[f"translations_desc_in_{langCode}"] = df["translations"].apply(lambda x: x.get("description", None)).apply(lambda x: x.get(langCode, None))
234 except AttributeError:
235 df[f"translations_name_in_{langCode}"] = None
236 df[f"translations_desc_in_{langCode}"] = None
237 df = df.drop(columns=["translations"])
239 result_df = df[selected_columns]
240 else:
241 # Call instatus API and get the response as pd.DataFrame
242 page_size = 100
243 # Calculate the number of pages required
244 page_count = (limit + page_size - 1) // page_size if limit else 1
245 result_df = pd.DataFrame(columns=selected_columns)
247 # Call instatus API and get the response as pd.DataFrame for each page
248 for page in range(1, page_count + 1):
249 current_page_size = min(page_size, limit) if limit else page_size
251 df = self.handler.call_instatus_api(endpoint=f'/v1/{pageId}/components', params={'page': page, 'per_page': current_page_size})
252 # Break if no more data is available or limit is reached
253 if len(df) == 0 or (limit and limit <= 0) or limit == 0:
254 break
255 ''' Add translations_name_in_{langCode} and translations_desc_in_{langCode} columns to the dataframe'''
256 for i in range(len(df)):
257 for langCode in langCodes:
258 try:
259 df.at[i, f"translations_name_in_{langCode}"] = df.at[i, "translations"].get("name", {}).get(langCode, None)
260 df.at[i, f"translations_desc_in_{langCode}"] = df.at[i, "translations"].get("description", {}).get(langCode, None)
261 except AttributeError:
262 df.at[i, f"translations_name_in_{langCode}"] = None
263 df.at[i, f"translations_desc_in_{langCode}"] = None
265 # Drop the 'translations' column
266 df = df.drop(columns=["translations"])
267 # Concatenate the dataframes
268 result_df = pd.concat([result_df, df[selected_columns]], ignore_index=True)
270 if limit:
271 limit -= len(df)
273 return result_df
275 def insert(self, query: ast.Insert) -> None:
276 """Receive query as AST (abstract syntax tree) and act upon it somehow.
278 Args:
279 query (ASTNode): sql query represented as AST. Usually it should be ast.Insert
281 Returns:
282 None
283 """
284 data = {'translations': {
285 "name": {},
286 "description": {}
287 }}
289 for column, value in zip(query.columns, query.values[0]):
290 if isinstance(value, Constant):
291 data[column.name] = json.loads(value.value) if column.name == 'translations' else value.value
292 elif isinstance(value, str):
293 try:
294 if re.match(r'^translations_name_in_[a-zA-Z\-]+$', column.name):
295 lang_code = column.name.split('_')[-1]
296 if lang_code not in langCodes:
297 raise Exception(f'Invalid language code {lang_code}')
298 data['translations']['name'][lang_code] = value
299 elif re.match(r'^translations_desc_in_[a-zA-Z\-]+$', column.name):
300 lang_code = column.name.split('_')[-1]
301 if lang_code not in langCodes:
302 raise Exception(f'Invalid language code {lang_code}')
303 data['translations']['description'][lang_code] = value
304 else:
305 data[column.name] = json.loads(value)
306 except json.JSONDecodeError:
307 data[column.name] = True if value == 'True' else (False if value == 'False' else value)
309 page_id = data.pop('page_id', None)
311 if page_id is not None:
312 self.handler.call_instatus_api(endpoint=f'/v1/{page_id}/components', method='POST', json_data=data)
314 def update(self, query: ast.Update) -> None:
315 """Receive query as AST (abstract syntax tree) and act upon it somehow.
317 Args:
318 query (ASTNode): sql query represented as AST. Usually it should be ast.Update
319 Returns:
320 None
321 """
322 conditions = extract_comparison_conditions(query.where)
323 # Get page id and component id from query
324 pageId = None
325 componentId = None
326 for condition in conditions:
327 if condition[1] == 'page_id' and condition[0] == '=':
328 pageId = condition[2]
329 elif condition[1] == 'component_id' and condition[0] == '=':
330 componentId = condition[2]
331 else:
332 raise Exception("page_id and component_id both are required")
334 data = {'translations': {
335 "name": {},
336 "description": {}
337 }}
338 for key, value in query.update_columns.items():
339 if isinstance(value, Constant):
340 if re.match(r'^translations_name_in_[a-zA-Z\-]+$', key):
341 lang_code = key.split('_')[-1]
342 if lang_code not in langCodes:
343 raise Exception(f'Invalid language code {lang_code}')
344 data['translations']['name'][lang_code] = value.value
345 elif re.match(r'^translations_desc_in_[a-zA-Z\-]+$', key):
346 lang_code = key.split('_')[-1]
347 if lang_code not in langCodes:
348 raise Exception(f'Invalid language code {lang_code}')
349 data['translations']['description'][lang_code] = value.value
350 else:
351 data[key] = value.value
352 self.handler.call_instatus_api(endpoint=f'/v1/{pageId}/components/{componentId}', method='PUT', json_data=data)
354 def get_columns(self, ignore: List[str] = []) -> List[str]:
355 """columns
357 Args:
358 ignore (List[str], optional): exclusion items. Defaults to [].
360 Returns:
361 List[str]: available columns with `ignore` items removed from the list.
362 """
363 return [
364 "id",
365 "name",
366 "nameTranslationId",
367 "description",
368 "descriptionTranslationId",
369 "status",
370 "order",
371 "showUptime",
372 "createdAt",
373 "updatedAt",
374 "archivedAt",
375 "siteId",
376 "uniqueEmail",
377 "oldGroup",
378 "groupId",
379 "isParent",
380 "isCollapsed",
381 "monitorId",
382 "nameHtml",
383 "nameHtmlTranslationId",
384 "descriptionHtml",
385 "descriptionHtmlTranslationId",
386 "isThirdParty",
387 "thirdPartyStatus",
388 "thirdPartyComponentId",
389 "thirdPartyComponentServiceId",
390 "importedFromStatuspage",
391 "startDate",
392 "group",
393 ] + [f'translations_name_in_{langCode}' for langCode in langCodes] + [f'translations_desc_in_{langCode}' for langCode in langCodes]