commons-metrics 0.0.15__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,305 @@
1
+ import requests
2
+ import base64
3
+ from typing import List, Dict, Optional
4
+ import os
5
+ from .commons_repos_client import extract_package_version_from_pubspec, extract_package_version_from_package_json
6
+
7
+ class GitHubAPIClient:
8
+ """
9
+ Cliente para interactuar con la API de GitHub
10
+ """
11
+
12
+ def __init__(self, token: str, owner: str = None, repo: str = None):
13
+ """
14
+ Inicializa el cliente de GitHub API
15
+
16
+ Args:
17
+ token: Personal Access Token de GitHub
18
+ owner: Dueño del repositorio (ej: 'grupobancolombia-innersource') - Opcional
19
+ repo: Nombre del repositorio (ej: 'NU0066001_BDS_MOBILE_Lib') - Opcional
20
+ """
21
+ self.token = token
22
+ self.owner = owner
23
+ self.repo = repo
24
+ self.base_url = f"https://api.github.com/repos/{owner}/{repo}" if owner and repo else "https://api.github.com"
25
+ self.headers = {
26
+ "Authorization": f"token {token}",
27
+ "Accept": "application/vnd.github.v3+json"
28
+ }
29
+
30
+ def get_directory_contents(self, path: str = "") -> List[Dict]:
31
+ """
32
+ Obtiene el contenido de un directorio en el repositorio
33
+
34
+ Args:
35
+ path: Ruta del directorio (ej: 'lib/atoms')
36
+
37
+ Returns:
38
+ Lista de diccionarios con información de archivos/carpetas
39
+ """
40
+ url = f"{self.base_url}/contents/{path}"
41
+ response = requests.get(url, headers=self.headers)
42
+
43
+ if response.status_code == 200:
44
+ return response.json()
45
+ elif response.status_code == 404:
46
+ return []
47
+ else:
48
+ raise Exception(f"Error getting directory contents: {response.status_code} - {response.text}")
49
+
50
+ def get_file_content(self, path: str) -> Optional[str]:
51
+ """
52
+ Obtiene el contenido de un archivo
53
+
54
+ Args:
55
+ path: Ruta del archivo en el repositorio
56
+
57
+ Returns:
58
+ Contenido del archivo como string, o None si no existe
59
+ """
60
+ url = f"{self.base_url}/contents/{path}"
61
+ response = requests.get(url, headers=self.headers)
62
+
63
+ if response.status_code == 200:
64
+ content = response.json()
65
+ if content.get('encoding') == 'base64':
66
+ decoded_content = base64.b64decode(content['content']).decode('utf-8')
67
+ return decoded_content
68
+ return None
69
+ elif response.status_code == 404:
70
+ return None
71
+ else:
72
+ raise Exception(f"Error getting file content: {response.status_code} - {response.text}")
73
+
74
+ def list_folders_in_directory(self, path: str) -> List[str]:
75
+ """
76
+ Lista solo las carpetas dentro de un directorio
77
+
78
+ Args:
79
+ path: Ruta del directorio
80
+
81
+ Returns:
82
+ Lista de nombres de carpetas
83
+ """
84
+ contents = self.get_directory_contents(path)
85
+ folders = [
86
+ item['name']
87
+ for item in contents
88
+ if item['type'] == 'dir'
89
+ ]
90
+ return folders
91
+
92
+ def walk_directory(self, path: str = "", extension: str = None, exclude_patterns: List[str] = None) -> List[Dict]:
93
+ """
94
+ Recorre recursivamente un directorio y retorna todos los archivos
95
+
96
+ Args:
97
+ path: Ruta del directorio inicial
98
+ extension: Extensión a filtrar (ej: '.ts', '.dart')
99
+ exclude_patterns: Lista de patrones a excluir (ej: ['.spec.', '.test.', '.d.ts'])
100
+
101
+ Returns:
102
+ Lista de diccionarios con información de archivos encontrados
103
+ """
104
+ all_files = []
105
+ exclude_patterns = exclude_patterns or []
106
+
107
+ def should_exclude(filename: str) -> bool:
108
+ return any(pattern in filename for pattern in exclude_patterns)
109
+
110
+ def recurse_directory(current_path: str):
111
+ contents = self.get_directory_contents(current_path)
112
+
113
+ for item in contents:
114
+ item_path = f"{current_path}/{item['name']}" if current_path else item['name']
115
+
116
+ if item['type'] == 'file':
117
+ # Aplicar filtros de extensión y exclusión
118
+ if extension and not item['name'].endswith(extension):
119
+ continue
120
+ if should_exclude(item['name']):
121
+ continue
122
+
123
+ all_files.append({
124
+ 'name': item['name'],
125
+ 'path': item_path,
126
+ 'url': item.get('url', ''),
127
+ 'download_url': item.get('download_url', '')
128
+ })
129
+
130
+ elif item['type'] == 'dir':
131
+ # Excluir directorios comunes que no contienen componentes
132
+ if item['name'] not in ['node_modules', 'dist', 'build', '.git', 'test', 'tests', '__pycache__']:
133
+ recurse_directory(item_path)
134
+
135
+ recurse_directory(path)
136
+ return all_files
137
+
138
+ def search_code(self, query: str, per_page: int = 100) -> List[Dict]:
139
+ """
140
+ Busca código en GitHub usando la API de búsqueda
141
+
142
+ Args:
143
+ query: Query de búsqueda (ej: '"bds_mobile" in:file filename:pubspec.yaml')
144
+ per_page: Número de resultados por página (máximo 100)
145
+
146
+ Returns:
147
+ Lista de diccionarios con información de archivos encontrados
148
+ """
149
+ all_results = []
150
+ page = 1
151
+
152
+ while True:
153
+ url = "https://api.github.com/search/code"
154
+ params = {
155
+ 'q': query,
156
+ 'per_page': per_page,
157
+ 'page': page
158
+ }
159
+
160
+ response = requests.get(url, headers=self.headers, params=params)
161
+
162
+ if response.status_code == 200:
163
+ data = response.json()
164
+ items = data.get('items', [])
165
+
166
+ if not items:
167
+ break
168
+
169
+ all_results.extend(items)
170
+
171
+ # Si hay menos items que per_page, es la última página
172
+ if len(items) < per_page:
173
+ break
174
+
175
+ page += 1
176
+ elif response.status_code == 403:
177
+ # Rate limit alcanzado
178
+ print(f"⚠️ Rate limit alcanzado. Resultados obtenidos: {len(all_results)}")
179
+ break
180
+ else:
181
+ raise Exception(f"Error searching code: {response.status_code} - {response.text}")
182
+
183
+ return all_results
184
+
185
+ def search_projects_with_bds(self, platform: str, design_system_name: str = None) -> List[Dict]:
186
+ """
187
+ Busca proyectos que usan el sistema de diseño BDS (mobile o web)
188
+
189
+ Args:
190
+ platform: 'mobile' o 'web'
191
+ design_system_name: Nombre del paquete del sistema de diseño
192
+ Si no se proporciona, usa valores por defecto:
193
+ - mobile: "bds_mobile"
194
+ - web: "@bancolombia/design-system-web"
195
+
196
+ Returns:
197
+ Lista de proyectos con información del repositorio incluyendo la versión
198
+
199
+ Raises:
200
+ ValueError: Si platform no es 'mobile' o 'web'
201
+ """
202
+ # Validar plataforma
203
+ if platform not in ['mobile', 'web']:
204
+ raise ValueError(f"Platform debe ser 'mobile' o 'web', se recibió: {platform}")
205
+
206
+ # Configurar valores según la plataforma
207
+ if platform == 'mobile':
208
+ config_file = 'pubspec.yaml'
209
+ default_package_name = 'bds_mobile'
210
+ extract_version_method = extract_package_version_from_pubspec
211
+ else: # web
212
+ config_file = 'package.json'
213
+ default_package_name = '@bancolombia/design-system-web'
214
+ extract_version_method = extract_package_version_from_package_json
215
+
216
+ # Usar nombre de paquete por defecto si no se proporciona
217
+ package_name = design_system_name or default_package_name
218
+
219
+ # Agregar filtro de organización si self.owner está definido
220
+ org_filter = f" org:{self.owner}" if self.owner else ""
221
+ query = f'"{package_name}" in:file filename:{config_file}{org_filter}'
222
+ results = self.search_code(query)
223
+
224
+ projects = []
225
+ for item in results:
226
+ # Obtener contenido del archivo de configuración para extraer la versión
227
+ file_content = self._get_file_content_from_url(item['url'])
228
+ version = extract_version_method(file_content, package_name)
229
+
230
+ project_info = {
231
+ 'name': item['repository']['name'],
232
+ 'full_name': item['repository']['full_name'],
233
+ 'owner': item['repository']['owner']['login'],
234
+ 'repo_url': item['repository']['html_url'],
235
+ 'file_path': item['path'],
236
+ 'bds_version': version
237
+ }
238
+ projects.append(project_info)
239
+
240
+ return projects
241
+
242
+ def search_repositories(self, query: str, per_page: int = 100) -> List[Dict]:
243
+ """
244
+ Busca repositorios en GitHub usando la API de búsqueda
245
+
246
+ Args:
247
+ query: Query de búsqueda (ej: 'NU0296001 mobile in:name org:grupobancolombia-innersource')
248
+ per_page: Número de resultados por página (máximo 100)
249
+
250
+ Returns:
251
+ Lista de diccionarios con información de repositorios encontrados
252
+ """
253
+ all_results = []
254
+ page = 1
255
+ search_url = "https://api.github.com/search/repositories"
256
+
257
+ while True:
258
+ params = {
259
+ 'q': query,
260
+ 'per_page': per_page,
261
+ 'page': page
262
+ }
263
+
264
+ response = requests.get(search_url, headers=self.headers, params=params)
265
+
266
+ if response.status_code != 200:
267
+ print(f"Error searching repositories: {response.status_code}")
268
+ print(f"Response: {response.text}")
269
+ break
270
+
271
+ data = response.json()
272
+ items = data.get('items', [])
273
+
274
+ if not items:
275
+ break
276
+
277
+ all_results.extend(items)
278
+
279
+ # Verificar si hay más páginas
280
+ if len(items) < per_page:
281
+ break
282
+
283
+ page += 1
284
+
285
+ return all_results
286
+
287
+ def _get_file_content_from_url(self, api_url: str) -> Optional[str]:
288
+ """
289
+ Obtiene el contenido de un archivo desde una URL de la API de GitHub
290
+
291
+ Args:
292
+ api_url: URL de la API de GitHub para el archivo
293
+
294
+ Returns:
295
+ Contenido del archivo como string, o None si no existe
296
+ """
297
+ response = requests.get(api_url, headers=self.headers)
298
+
299
+ if response.status_code == 200:
300
+ content = response.json()
301
+ if content.get('encoding') == 'base64':
302
+ decoded_content = base64.b64decode(content['content']).decode('utf-8')
303
+ return decoded_content
304
+ return None
305
+ return None
@@ -0,0 +1,269 @@
1
+ from typing import Optional
2
+
3
+ class ComponentRepository:
4
+ """
5
+ Repository class for component-specific database operations
6
+ """
7
+ def __init__(self, db_connection):
8
+ self.db = db_connection
9
+
10
+ def save_component(self, technical_name, functional_name, id_type, folder, class_name, status):
11
+ cursor = None
12
+ try:
13
+ cursor = self.db.connection.cursor()
14
+
15
+ insert_query = """
16
+ INSERT INTO schmetrc.component(technical_name,functional_name, type_id, folder, class_name, status)
17
+ VALUES (%s, %s, %s, %s, %s, %s)
18
+ RETURNING id;
19
+ """
20
+
21
+ cursor.execute(insert_query, (technical_name, functional_name, id_type, folder, class_name, status))
22
+ component_id = cursor.fetchone()[0]
23
+ self.db.commit_transaction()
24
+
25
+ return component_id
26
+
27
+ except Exception as e:
28
+ self.db.rollback_transaction()
29
+ raise Exception(f"Error saving component '{technical_name}': {str(e)}")
30
+ finally:
31
+ if cursor:
32
+ cursor.close()
33
+
34
+ def query_components_by_type(self, id_type: int) -> list:
35
+ """
36
+ Consulta componentes de un tipo específico en la base de datos
37
+
38
+ Args:
39
+ id_type (int): ID del tipo de componente a consultar
40
+ """
41
+ cursor = None
42
+ try:
43
+ cursor = self.db.connection.cursor()
44
+
45
+ query = """
46
+ SELECT id, technical_name, type_id, status, functional_name, folder, class_name
47
+ FROM schmetrc.component
48
+ WHERE type_id = %s
49
+ ORDER BY technical_name
50
+ """
51
+
52
+ cursor.execute(query, (id_type,))
53
+ results = cursor.fetchall()
54
+
55
+ components = []
56
+ for row in results:
57
+ component = {
58
+ 'id': row[0],
59
+ 'technical_name': row[1],
60
+ 'type_id': row[2],
61
+ 'status': row[3],
62
+ 'functional_name': row[4],
63
+ 'folder': row[5],
64
+ 'class_name': row[6]
65
+ }
66
+ components.append(component)
67
+
68
+ return components
69
+
70
+ except Exception as e:
71
+ raise Exception(f"Error querying components by type {id_type}: {str(e)}")
72
+ finally:
73
+ if cursor:
74
+ cursor.close()
75
+
76
+ def query_versions_by_type(self, type_id: int) -> list:
77
+ """
78
+ Consulta versiones de un tipo específico (mobile o web) en la base de datos
79
+
80
+ Args:
81
+ type_id (int): ID del tipo (13 para mobile, 12 para web)
82
+
83
+ Returns:
84
+ list: Lista de versiones con sus IDs
85
+ """
86
+ cursor = None
87
+ try:
88
+ cursor = self.db.connection.cursor()
89
+ query = """
90
+ SELECT id, version, type_id, creation_date
91
+ FROM schmetrc.general_version
92
+ WHERE type_id = %s
93
+ ORDER BY version DESC
94
+ """
95
+ cursor.execute(query, (type_id,))
96
+ results = cursor.fetchall()
97
+
98
+ # Get column names from cursor description
99
+ column_names = [desc[0] for desc in cursor.description]
100
+
101
+ versions = []
102
+ for row in results:
103
+ # Crear versión dinámicamente usando los nombres de columnas
104
+ version = dict(zip(column_names, row))
105
+ versions.append(version)
106
+
107
+ return versions
108
+
109
+ except Exception as e:
110
+ raise RuntimeError(f"Error querying versions by type {type_id}: {str(e)}")
111
+ finally:
112
+ if cursor:
113
+ cursor.close()
114
+
115
+ def save_adoption(self, version_id: int, component_id: int, aplication_code: str, count: int, historical_metric_id: int = None):
116
+ """
117
+ Inserta un registro de adopción en la tabla schmetrc.general_adoption
118
+
119
+ Args:
120
+ version_id (int): ID de la versión del sistema de diseño
121
+ component_id (int): ID del componente
122
+ aplication_code (str): Código de aplicación (ej: NU0296001)
123
+ count (int): Cantidad de usos del componente
124
+ historical_metric_id (int, optional): ID de la fecha en measurement_systems_history. Por defecto None
125
+ """
126
+ cursor = None
127
+ try:
128
+ cursor = self.db.connection.cursor()
129
+ insert_query = """
130
+ INSERT INTO schmetrc.general_adoption(version_id, component_id, aplication_code, count, historical_metric_id)
131
+ VALUES (%s, %s, %s, %s, %s)
132
+ RETURNING id;
133
+ """
134
+
135
+ cursor.execute(insert_query, (version_id, component_id, aplication_code, count, historical_metric_id))
136
+ adoption_id = cursor.fetchone()[0]
137
+ self.db.commit_transaction()
138
+ return adoption_id
139
+ except Exception as e:
140
+ self.db.rollback_transaction()
141
+ raise RuntimeError(f"Error saving adoption for component {component_id}: {str(e)}")
142
+ finally:
143
+ if cursor:
144
+ cursor.close()
145
+
146
+ def get_or_create_current_month_date(self) -> int:
147
+ """
148
+ Obtiene el ID de la fecha del mes actual en measurement_systems_history.
149
+ Si no existe, crea un registro con la fecha de hoy.
150
+
151
+ Returns:
152
+ int: ID del registro de fecha
153
+ """
154
+ cursor = None
155
+ try:
156
+ cursor = self.db.connection.cursor()
157
+
158
+ # Buscar si existe un registro del mes actual
159
+ query_select = """
160
+ SELECT id
161
+ FROM schmetrc.measurement_systems_history
162
+ WHERE EXTRACT(YEAR FROM date) = EXTRACT(YEAR FROM CURRENT_DATE)
163
+ AND EXTRACT(MONTH FROM date) = EXTRACT(MONTH FROM CURRENT_DATE)
164
+ LIMIT 1
165
+ """
166
+ cursor.execute(query_select)
167
+ result = cursor.fetchone()
168
+
169
+ if result:
170
+ # Ya existe un registro del mes actual
171
+ date_id = result[0]
172
+ print(f" ℹ️ Usando fecha existente del mes actual (ID: {date_id})")
173
+ return date_id
174
+ else:
175
+ # No existe, crear uno nuevo con la fecha de hoy
176
+ query_insert = """
177
+ INSERT INTO schmetrc.measurement_systems_history (date)
178
+ VALUES (CURRENT_DATE)
179
+ RETURNING id
180
+ """
181
+ cursor.execute(query_insert)
182
+ date_id = cursor.fetchone()[0]
183
+ self.db.commit_transaction()
184
+ print(f" ✅ Creado nuevo registro de fecha (ID: {date_id})")
185
+ return date_id
186
+
187
+ except Exception as e:
188
+ self.db.rollback_transaction()
189
+ raise RuntimeError(f"Error getting or creating current month date: {str(e)}")
190
+ finally:
191
+ if cursor:
192
+ cursor.close()
193
+
194
+ def delete_adoption_by_date_id(self, date_id: int, type_id: int = None) -> int:
195
+ """
196
+ Elimina todos los registros de adopción asociados a un historical_metric_id específico.
197
+ Opcionalmente filtra por tipo de componente (mobile=13, web=12).
198
+ Útil para limpiar datos del mes actual antes de una nueva inserción.
199
+
200
+ Args:
201
+ date_id (int): ID de la fecha cuyos registros se eliminarán (historical_metric_id)
202
+ type_id (int, optional): ID del tipo de componente para filtrar. Si es None, elimina todos los tipos.
203
+
204
+ Returns:
205
+ int: Cantidad de registros eliminados
206
+ """
207
+ cursor = None
208
+ try:
209
+ cursor = self.db.connection.cursor()
210
+
211
+ # Construir query con o sin filtro de tipo
212
+ if type_id is not None:
213
+ # Contar registros a eliminar (con filtro de tipo)
214
+ count_query = """
215
+ SELECT COUNT(*)
216
+ FROM schmetrc.general_adoption ga
217
+ INNER JOIN schmetrc.component c ON ga.component_id = c.id
218
+ WHERE ga.historical_metric_id = %s AND c.type_id = %s
219
+ """
220
+ cursor.execute(count_query, (date_id, type_id))
221
+ count = cursor.fetchone()[0]
222
+
223
+ if count > 0:
224
+ # Eliminar los registros (con filtro de tipo)
225
+ delete_query = """
226
+ DELETE FROM schmetrc.general_adoption
227
+ WHERE id IN (
228
+ SELECT ga.id
229
+ FROM schmetrc.general_adoption ga
230
+ INNER JOIN schmetrc.component c ON ga.component_id = c.id
231
+ WHERE ga.historical_metric_id = %s AND c.type_id = %s
232
+ )
233
+ """
234
+ cursor.execute(delete_query, (date_id, type_id))
235
+ self.db.commit_transaction()
236
+ type_name = "mobile" if type_id == 13 else "web" if type_id == 12 else f"tipo {type_id}"
237
+ print(f" 🗑️ Eliminados {count} registros de adopción {type_name} del mes actual")
238
+ else:
239
+ print(" ℹ️ No hay registros previos de adopción para eliminar")
240
+ else:
241
+ # Contar registros a eliminar (sin filtro de tipo)
242
+ count_query = """
243
+ SELECT COUNT(*)
244
+ FROM schmetrc.general_adoption
245
+ WHERE historical_metric_id = %s
246
+ """
247
+ cursor.execute(count_query, (date_id,))
248
+ count = cursor.fetchone()[0]
249
+
250
+ if count > 0:
251
+ # Eliminar los registros (sin filtro de tipo)
252
+ delete_query = """
253
+ DELETE FROM schmetrc.general_adoption
254
+ WHERE historical_metric_id = %s
255
+ """
256
+ cursor.execute(delete_query, (date_id,))
257
+ self.db.commit_transaction()
258
+ print(f" 🗑️ Eliminados {count} registros de adopción del mes actual")
259
+ else:
260
+ print(" ℹ️ No hay registros previos de adopción para eliminar")
261
+
262
+ return count
263
+
264
+ except Exception as e:
265
+ self.db.rollback_transaction()
266
+ raise RuntimeError(f"Error deleting adoption records for date_id {date_id}: {str(e)}")
267
+ finally:
268
+ if cursor:
269
+ cursor.close()