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.
- commons_metrics/__init__.py +9 -0
- commons_metrics/azure_devops_client.py +432 -0
- commons_metrics/commons_repos_client.py +114 -0
- commons_metrics/database.py +39 -0
- commons_metrics/github_api_client.py +305 -0
- commons_metrics/repositories.py +269 -0
- commons_metrics/update_design_components.py +203 -0
- commons_metrics/util.py +38 -0
- commons_metrics-0.0.15.dist-info/METADATA +17 -0
- commons_metrics-0.0.15.dist-info/RECORD +13 -0
- commons_metrics-0.0.15.dist-info/WHEEL +5 -0
- commons_metrics-0.0.15.dist-info/licenses/LICENSE +9 -0
- commons_metrics-0.0.15.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
from .util import Util
|
|
2
|
+
from .database import DatabaseConnection
|
|
3
|
+
from .repositories import ComponentRepository
|
|
4
|
+
from .update_design_components import UpdateDesignSystemComponents
|
|
5
|
+
from .github_api_client import GitHubAPIClient
|
|
6
|
+
from .azure_devops_client import AzureDevOpsClient
|
|
7
|
+
|
|
8
|
+
__all__ = ['Util', 'DatabaseConnection', 'ComponentRepository', 'UpdateDesignSystemComponents', 'GitHubAPIClient', 'AzureDevOpsClient']
|
|
9
|
+
__version__ = '0.0.15'
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import time
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
from typing import List, Dict, Optional
|
|
6
|
+
from requests.exceptions import HTTPError, Timeout, ConnectionError
|
|
7
|
+
import urllib3
|
|
8
|
+
from .commons_repos_client import extract_package_version_from_pubspec, extract_package_version_from_package_json
|
|
9
|
+
|
|
10
|
+
# Suprimir warnings de SSL para Azure DevOps con certificados corporativos
|
|
11
|
+
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class AzureDevOpsClient:
|
|
15
|
+
"""
|
|
16
|
+
Cliente para interactuar con la API de Azure DevOps
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self, token: str, organization: str = "grupobancolombia",
|
|
20
|
+
project_id: str = "b267af7c-3233-4ad1-97b3-91083943100d"):
|
|
21
|
+
"""
|
|
22
|
+
Inicializa el cliente de Azure DevOps API
|
|
23
|
+
|
|
24
|
+
Args:
|
|
25
|
+
token: Bearer token de Azure DevOps
|
|
26
|
+
organization: Nombre de la organización (default: 'grupobancolombia')
|
|
27
|
+
project_id: ID del proyecto VSTI (default: ID de Vicepresidencia Servicios de Tecnología)
|
|
28
|
+
"""
|
|
29
|
+
self.token = token
|
|
30
|
+
self.organization = organization
|
|
31
|
+
self.project_id = project_id
|
|
32
|
+
|
|
33
|
+
# URLs de Azure DevOps
|
|
34
|
+
self.base_url = f"https://{organization}.visualstudio.com/"
|
|
35
|
+
self.search_url = f"https://{organization}.almsearch.visualstudio.com/"
|
|
36
|
+
self.api_path = "_apis/"
|
|
37
|
+
|
|
38
|
+
# Headers de autenticación
|
|
39
|
+
self.headers = {
|
|
40
|
+
"Accept": "application/json;api-version=5.0-preview.1",
|
|
41
|
+
"Authorization": f"Bearer {token}",
|
|
42
|
+
"Content-Type": "application/json"
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
# Configuración
|
|
46
|
+
self.max_results_per_page = 1000
|
|
47
|
+
self.verify_ssl = False # Desactivar para certificados corporativos
|
|
48
|
+
self.excluded_repos = ["deprecated", "test", "prueba", "poc", "jubilado"]
|
|
49
|
+
|
|
50
|
+
def _requests_with_retries(self, method: str, url: str,
|
|
51
|
+
headers: Dict = None, params: Dict = None,
|
|
52
|
+
json_body: Dict = None, timeout: tuple = (10, 300),
|
|
53
|
+
retries: int = 5, backoff_factor: float = 0.5):
|
|
54
|
+
"""
|
|
55
|
+
Realiza peticiones HTTP con reintentos automáticos
|
|
56
|
+
|
|
57
|
+
Args:
|
|
58
|
+
method: Método HTTP ('GET' o 'POST')
|
|
59
|
+
url: URL a consultar
|
|
60
|
+
headers: Headers de la petición
|
|
61
|
+
params: Parámetros de la petición (para GET)
|
|
62
|
+
json_body: Body JSON (para POST)
|
|
63
|
+
timeout: Timeout de conexión y lectura
|
|
64
|
+
retries: Número de reintentos
|
|
65
|
+
backoff_factor: Factor de espera exponencial
|
|
66
|
+
|
|
67
|
+
Returns:
|
|
68
|
+
Response object
|
|
69
|
+
"""
|
|
70
|
+
headers = headers or self.headers
|
|
71
|
+
|
|
72
|
+
for attempt in range(retries):
|
|
73
|
+
try:
|
|
74
|
+
if method.upper() == 'GET':
|
|
75
|
+
response = requests.get(
|
|
76
|
+
url,
|
|
77
|
+
headers=headers,
|
|
78
|
+
params=params,
|
|
79
|
+
timeout=timeout,
|
|
80
|
+
verify=self.verify_ssl
|
|
81
|
+
)
|
|
82
|
+
elif method.upper() == 'POST':
|
|
83
|
+
response = requests.post(
|
|
84
|
+
url,
|
|
85
|
+
headers=headers,
|
|
86
|
+
json=json_body,
|
|
87
|
+
timeout=timeout,
|
|
88
|
+
verify=self.verify_ssl
|
|
89
|
+
)
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(f"Método HTTP no soportado: {method}")
|
|
92
|
+
|
|
93
|
+
response.raise_for_status()
|
|
94
|
+
return response
|
|
95
|
+
|
|
96
|
+
except (Timeout, ConnectionError, HTTPError) as e:
|
|
97
|
+
if isinstance(e, HTTPError) and e.response.status_code < 500:
|
|
98
|
+
raise e
|
|
99
|
+
if attempt < retries - 1:
|
|
100
|
+
wait_time = backoff_factor * (2 ** attempt)
|
|
101
|
+
print(f"⚠️ Intento {attempt + 1}/{retries} fallido. Reintentando en {wait_time}s...")
|
|
102
|
+
time.sleep(wait_time)
|
|
103
|
+
else:
|
|
104
|
+
raise ConnectionError(f"No se pudo conectar a {url} después de {retries} intentos.")
|
|
105
|
+
|
|
106
|
+
def _search_repos_with_file(self, filename: str,
|
|
107
|
+
project_filter: str = "Vicepresidencia Servicios de Tecnología") -> List[Dict]:
|
|
108
|
+
"""
|
|
109
|
+
Busca repositorios que contengan un archivo específico
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
filename: Nombre del archivo a buscar (ej: 'pubspec.yaml', 'package.json')
|
|
113
|
+
project_filter: Filtro de proyecto de Azure DevOps
|
|
114
|
+
|
|
115
|
+
Returns:
|
|
116
|
+
Lista de repositorios con nombre, ID y metadata
|
|
117
|
+
"""
|
|
118
|
+
print(f"🔍 Buscando repositorios en Azure DevOps con archivo '{filename}'...")
|
|
119
|
+
|
|
120
|
+
search_expression = f'file:{filename}'
|
|
121
|
+
all_repo_info = set()
|
|
122
|
+
skip_results = 0
|
|
123
|
+
|
|
124
|
+
api_url = f"{self.search_url}{self.project_id}/{self.api_path}search/codeQueryResults"
|
|
125
|
+
|
|
126
|
+
while True:
|
|
127
|
+
json_body = {
|
|
128
|
+
"searchText": search_expression,
|
|
129
|
+
"skipResults": skip_results,
|
|
130
|
+
"takeResults": self.max_results_per_page,
|
|
131
|
+
"summarizedHitCountsNeeded": True,
|
|
132
|
+
"searchFilters": {
|
|
133
|
+
"ProjectFilters": [project_filter]
|
|
134
|
+
},
|
|
135
|
+
"filters": [],
|
|
136
|
+
"includeSuggestions": False,
|
|
137
|
+
"sortOptions": []
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
response = self._requests_with_retries(
|
|
142
|
+
"POST",
|
|
143
|
+
api_url,
|
|
144
|
+
json_body=json_body
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
# Validar el status code antes de parsear
|
|
148
|
+
if response.status_code != 200:
|
|
149
|
+
print(f"❌ Error HTTP {response.status_code}: {response.text[:500]}")
|
|
150
|
+
break
|
|
151
|
+
|
|
152
|
+
# Intentar parsear el JSON
|
|
153
|
+
try:
|
|
154
|
+
data = response.json()
|
|
155
|
+
except json.JSONDecodeError as json_err:
|
|
156
|
+
print(f"❌ Error parseando respuesta JSON: {json_err}")
|
|
157
|
+
print(f" Respuesta raw (primeros 500 caracteres): {response.text[:500]}")
|
|
158
|
+
break
|
|
159
|
+
|
|
160
|
+
results_wrapper = data.get("results", {})
|
|
161
|
+
|
|
162
|
+
# Manejar diferentes estructuras de respuesta
|
|
163
|
+
if isinstance(results_wrapper, dict):
|
|
164
|
+
results = results_wrapper.get("values", [])
|
|
165
|
+
elif isinstance(results_wrapper, list):
|
|
166
|
+
results = results_wrapper
|
|
167
|
+
else:
|
|
168
|
+
results = []
|
|
169
|
+
|
|
170
|
+
if not results:
|
|
171
|
+
break
|
|
172
|
+
|
|
173
|
+
# Extraer información de repositorios y rutas de archivos
|
|
174
|
+
for result in results:
|
|
175
|
+
if not isinstance(result, dict):
|
|
176
|
+
continue
|
|
177
|
+
|
|
178
|
+
repo_data = result.get("repository", {})
|
|
179
|
+
file_path = result.get("path", filename) # Obtener la ruta completa del archivo
|
|
180
|
+
|
|
181
|
+
if isinstance(repo_data, dict):
|
|
182
|
+
repo_name = repo_data.get("name")
|
|
183
|
+
repo_id = repo_data.get("id")
|
|
184
|
+
elif isinstance(repo_data, str):
|
|
185
|
+
repo_name = repo_data
|
|
186
|
+
repo_id = result.get("repositoryId")
|
|
187
|
+
else:
|
|
188
|
+
continue
|
|
189
|
+
|
|
190
|
+
if repo_name and repo_id:
|
|
191
|
+
# Guardar repo_id, repo_name y file_path
|
|
192
|
+
all_repo_info.add((repo_name, repo_id, file_path))
|
|
193
|
+
|
|
194
|
+
# Verificar si hay más páginas
|
|
195
|
+
if len(results) < self.max_results_per_page:
|
|
196
|
+
break
|
|
197
|
+
|
|
198
|
+
skip_results += len(results)
|
|
199
|
+
|
|
200
|
+
except Exception as e:
|
|
201
|
+
print(f"❌ Error en búsqueda de Azure: {e}")
|
|
202
|
+
break
|
|
203
|
+
|
|
204
|
+
# Filtrar repositorios excluidos
|
|
205
|
+
filtered_repos = []
|
|
206
|
+
for repo_name, repo_id, file_path in all_repo_info:
|
|
207
|
+
repo_lower = repo_name.lower()
|
|
208
|
+
should_exclude = any(word.lower() in repo_lower for word in self.excluded_repos)
|
|
209
|
+
|
|
210
|
+
if not should_exclude:
|
|
211
|
+
filtered_repos.append({
|
|
212
|
+
"name": repo_name,
|
|
213
|
+
"id": repo_id,
|
|
214
|
+
"file_path": file_path, # Agregar la ruta del archivo encontrado
|
|
215
|
+
"source": "azure"
|
|
216
|
+
})
|
|
217
|
+
|
|
218
|
+
print(f"✅ Encontrados {len(filtered_repos)} repositorios en Azure DevOps")
|
|
219
|
+
return sorted(filtered_repos, key=lambda r: r['name'])
|
|
220
|
+
|
|
221
|
+
def get_repo_files(self, repo_id: str, extension: str = None,
|
|
222
|
+
exclude_patterns: List[str] = None) -> List[Dict]:
|
|
223
|
+
"""
|
|
224
|
+
Obtiene todos los archivos de un repositorio
|
|
225
|
+
|
|
226
|
+
Args:
|
|
227
|
+
repo_id: ID del repositorio en Azure DevOps
|
|
228
|
+
extension: Extensión a filtrar (ej: '.dart', '.ts')
|
|
229
|
+
exclude_patterns: Lista de patrones a excluir (ej: ['test', '.spec.'])
|
|
230
|
+
|
|
231
|
+
Returns:
|
|
232
|
+
Lista de archivos con path, objectId y metadata
|
|
233
|
+
"""
|
|
234
|
+
exclude_patterns = exclude_patterns or []
|
|
235
|
+
items_url = f"{self.base_url}{self.project_id}/{self.api_path}git/repositories/{repo_id}/items"
|
|
236
|
+
params = {
|
|
237
|
+
"recursionLevel": "full",
|
|
238
|
+
"api-version": "7.1"
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
try:
|
|
242
|
+
response = self._requests_with_retries(
|
|
243
|
+
'GET',
|
|
244
|
+
items_url,
|
|
245
|
+
params=params,
|
|
246
|
+
timeout=(10, 600)
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
all_items = response.json().get("value", [])
|
|
250
|
+
filtered_files = []
|
|
251
|
+
|
|
252
|
+
for item in all_items:
|
|
253
|
+
# Solo archivos (blobs)
|
|
254
|
+
if item.get("gitObjectType") != "blob":
|
|
255
|
+
continue
|
|
256
|
+
|
|
257
|
+
path = item.get("path", "")
|
|
258
|
+
path_lower = path.lower()
|
|
259
|
+
|
|
260
|
+
# Filtrar por extensión si se especifica
|
|
261
|
+
if extension and not path_lower.endswith(extension.lower()):
|
|
262
|
+
continue
|
|
263
|
+
|
|
264
|
+
# Aplicar patrones de exclusión
|
|
265
|
+
should_exclude = any(pattern.lower() in path_lower for pattern in exclude_patterns)
|
|
266
|
+
if should_exclude:
|
|
267
|
+
continue
|
|
268
|
+
|
|
269
|
+
filtered_files.append({
|
|
270
|
+
'name': path.split('/')[-1],
|
|
271
|
+
'path': path,
|
|
272
|
+
'objectId': item.get('objectId'),
|
|
273
|
+
'url': item.get('url', '')
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
return filtered_files
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
print(f"❌ Error obteniendo archivos del repo {repo_id}: {e}")
|
|
280
|
+
return []
|
|
281
|
+
|
|
282
|
+
def get_file_content(self, repo_id: str, blob_id: str) -> Optional[str]:
|
|
283
|
+
"""
|
|
284
|
+
Obtiene el contenido de un archivo por su blob ID
|
|
285
|
+
|
|
286
|
+
Args:
|
|
287
|
+
repo_id: ID del repositorio
|
|
288
|
+
blob_id: ID del blob (archivo)
|
|
289
|
+
|
|
290
|
+
Returns:
|
|
291
|
+
Contenido del archivo como string, o None si hay error
|
|
292
|
+
"""
|
|
293
|
+
if not blob_id:
|
|
294
|
+
return None
|
|
295
|
+
|
|
296
|
+
content_url = f"{self.base_url}{self.project_id}/{self.api_path}git/repositories/{repo_id}/blobs/{blob_id}"
|
|
297
|
+
params = {
|
|
298
|
+
"api-version": "7.1",
|
|
299
|
+
"$format": "octetstream"
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
try:
|
|
303
|
+
response = self._requests_with_retries(
|
|
304
|
+
'GET',
|
|
305
|
+
content_url,
|
|
306
|
+
params=params,
|
|
307
|
+
timeout=(10, 180)
|
|
308
|
+
)
|
|
309
|
+
return response.text
|
|
310
|
+
|
|
311
|
+
except Exception as e:
|
|
312
|
+
print(f"⚠️ Error obteniendo contenido de archivo: {e}")
|
|
313
|
+
return None
|
|
314
|
+
|
|
315
|
+
def _get_file_content_by_path(self, repo_id: str, file_path: str) -> Optional[str]:
|
|
316
|
+
"""
|
|
317
|
+
Obtiene el contenido de un archivo por su ruta
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
repo_id: ID del repositorio
|
|
321
|
+
file_path: Ruta del archivo en el repositorio
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Contenido del archivo como string, o None si hay error
|
|
325
|
+
"""
|
|
326
|
+
# Primero obtener el objectId del archivo
|
|
327
|
+
items_url = f"{self.base_url}{self.project_id}/{self.api_path}git/repositories/{repo_id}/items"
|
|
328
|
+
params = {
|
|
329
|
+
"path": file_path,
|
|
330
|
+
"api-version": "7.1"
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
try:
|
|
334
|
+
response = self._requests_with_retries(
|
|
335
|
+
'GET',
|
|
336
|
+
items_url,
|
|
337
|
+
params=params,
|
|
338
|
+
timeout=(10, 60)
|
|
339
|
+
)
|
|
340
|
+
|
|
341
|
+
item_data = response.json()
|
|
342
|
+
object_id = item_data.get('objectId')
|
|
343
|
+
|
|
344
|
+
if object_id:
|
|
345
|
+
return self.get_file_content(repo_id, object_id)
|
|
346
|
+
|
|
347
|
+
return None
|
|
348
|
+
|
|
349
|
+
except HTTPError as e:
|
|
350
|
+
# Si es 404, el archivo no existe (es normal, no mostrar error)
|
|
351
|
+
if e.response.status_code == 404:
|
|
352
|
+
return None
|
|
353
|
+
# Otros errores HTTP sí se muestran
|
|
354
|
+
print(f"⚠️ Error HTTP {e.response.status_code} obteniendo {file_path}")
|
|
355
|
+
return None
|
|
356
|
+
except Exception as e:
|
|
357
|
+
# Otros errores generales
|
|
358
|
+
print(f"⚠️ Error obteniendo archivo por ruta: {e}")
|
|
359
|
+
return None
|
|
360
|
+
|
|
361
|
+
def search_projects_with_bds(self, platform: str, design_system_name: str = None) -> List[Dict]:
|
|
362
|
+
"""
|
|
363
|
+
Busca proyectos que usan el sistema de diseño BDS (mobile o web)
|
|
364
|
+
|
|
365
|
+
Args:
|
|
366
|
+
platform: 'mobile' o 'web'
|
|
367
|
+
design_system_name: Nombre del paquete del sistema de diseño
|
|
368
|
+
Si no se proporciona, usa valores por defecto:
|
|
369
|
+
- mobile: "bds_mobile"
|
|
370
|
+
- web: "@bancolombia/design-system-web"
|
|
371
|
+
|
|
372
|
+
Returns:
|
|
373
|
+
Lista de proyectos con información completa
|
|
374
|
+
|
|
375
|
+
Raises:
|
|
376
|
+
ValueError: Si platform no es 'mobile' o 'web'
|
|
377
|
+
"""
|
|
378
|
+
# Validar plataforma
|
|
379
|
+
if platform not in ['mobile', 'web']:
|
|
380
|
+
raise ValueError(f"Platform debe ser 'mobile' o 'web', se recibió: {platform}")
|
|
381
|
+
|
|
382
|
+
# Configurar valores según la plataforma
|
|
383
|
+
if platform == 'mobile':
|
|
384
|
+
config_file = 'pubspec.yaml'
|
|
385
|
+
default_package_name = 'bds_mobile'
|
|
386
|
+
extract_version_method = extract_package_version_from_pubspec
|
|
387
|
+
else: # web
|
|
388
|
+
config_file = 'package.json'
|
|
389
|
+
default_package_name = '@bancolombia/design-system-web'
|
|
390
|
+
extract_version_method = extract_package_version_from_package_json
|
|
391
|
+
|
|
392
|
+
# Usar nombre de paquete por defecto si no se proporciona
|
|
393
|
+
package_name = design_system_name or default_package_name
|
|
394
|
+
|
|
395
|
+
# Buscar repositorios con el archivo de configuración
|
|
396
|
+
repos = self._search_repos_with_file(config_file)
|
|
397
|
+
|
|
398
|
+
projects = []
|
|
399
|
+
for idx, repo in enumerate(repos, 1):
|
|
400
|
+
print(f" 📦 [{idx}/{len(repos)}] Verificando {repo['name']}...")
|
|
401
|
+
|
|
402
|
+
# Usar la ruta del archivo encontrado por la búsqueda (puede estar en subcarpetas)
|
|
403
|
+
file_path = repo.get('file_path', config_file)
|
|
404
|
+
|
|
405
|
+
# Obtener contenido del archivo usando la ruta completa
|
|
406
|
+
file_content = self._get_file_content_by_path(repo['id'], file_path)
|
|
407
|
+
|
|
408
|
+
if not file_content:
|
|
409
|
+
continue
|
|
410
|
+
|
|
411
|
+
# Verificar si usa BDS
|
|
412
|
+
if package_name not in file_content:
|
|
413
|
+
continue
|
|
414
|
+
|
|
415
|
+
# Extraer versión
|
|
416
|
+
version = extract_version_method(file_content, package_name)
|
|
417
|
+
|
|
418
|
+
project_info = {
|
|
419
|
+
'name': repo['name'],
|
|
420
|
+
'full_name': f"azure/{repo['name']}",
|
|
421
|
+
'owner': 'azure',
|
|
422
|
+
'repo_url': f"{self.base_url}{self.project_id}/_git/{repo['name']}",
|
|
423
|
+
'file_path': file_path,
|
|
424
|
+
'bds_version': version,
|
|
425
|
+
'source': 'azure',
|
|
426
|
+
'repo_id': repo['id']
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
projects.append(project_info)
|
|
430
|
+
|
|
431
|
+
print(f"\n✅ Total proyectos {platform} con BDS en Azure: {len(projects)}")
|
|
432
|
+
return projects
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Módulo con funciones compartidas entre clientes de repositorios (GitHub, Azure DevOps)
|
|
3
|
+
"""
|
|
4
|
+
import re
|
|
5
|
+
import json
|
|
6
|
+
from typing import Optional, Dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_package_version_from_pubspec(content: str, package_name: str) -> Optional[Dict]:
|
|
10
|
+
"""
|
|
11
|
+
Extrae la versión de un paquete del contenido de pubspec.yaml
|
|
12
|
+
|
|
13
|
+
Args:
|
|
14
|
+
content: Contenido del archivo pubspec.yaml
|
|
15
|
+
package_name: Nombre del paquete a buscar (ej: 'bds_mobile')
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
Diccionario con versión completa y major, o None
|
|
19
|
+
"""
|
|
20
|
+
if not content:
|
|
21
|
+
return None
|
|
22
|
+
|
|
23
|
+
# Patrones para diferentes formatos de versión (con posibles espacios antes)
|
|
24
|
+
# IMPORTANTE: debe estar en sección dependencies: para evitar falsos positivos
|
|
25
|
+
simple_patterns = [
|
|
26
|
+
# Formato simple: bds_mobile: ^8.127.0 (en la misma línea)
|
|
27
|
+
rf'^\s*{package_name}\s*:\s*\^?([0-9]+\.[0-9]+\.[0-9]+)',
|
|
28
|
+
# Formato con >= o ~
|
|
29
|
+
rf'^\s*{package_name}\s*:\s*>=([0-9]+\.[0-9]+\.[0-9]+)',
|
|
30
|
+
rf'^\s*{package_name}\s*:\s*~>([0-9]+\.[0-9]+\.[0-9]+)',
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
for pattern in simple_patterns:
|
|
34
|
+
match = re.search(pattern, content, re.MULTILINE)
|
|
35
|
+
if match:
|
|
36
|
+
version = match.group(1)
|
|
37
|
+
major_version = version.split('.')[0]
|
|
38
|
+
return {
|
|
39
|
+
'full_version': version,
|
|
40
|
+
'major_version': major_version
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
# Formato hosted con artifactory (múltiples variantes)
|
|
44
|
+
hosted_patterns = [
|
|
45
|
+
# Patrón 1: hosted simple (una línea)
|
|
46
|
+
rf'^(\s*){package_name}\s*:\s*$\s*\1\s+hosted:\s*https?://.*?$\s*\1\s+version:\s*["\']?\^?([0-9]+\.[0-9]+\.[0-9]+)',
|
|
47
|
+
# Patrón 2: hosted con name/url (multilínea)
|
|
48
|
+
rf'^(\s*){package_name}\s*:\s*$\s*\1\s+hosted:\s*$.*?\s*\1\s+version:\s*["\']?\^?([0-9]+\.[0-9]+\.[0-9]+)',
|
|
49
|
+
]
|
|
50
|
+
|
|
51
|
+
for hosted_pattern in hosted_patterns:
|
|
52
|
+
hosted_match = re.search(hosted_pattern, content, re.MULTILINE | re.DOTALL)
|
|
53
|
+
if hosted_match:
|
|
54
|
+
# El último grupo siempre es la versión
|
|
55
|
+
version = hosted_match.groups()[-1]
|
|
56
|
+
major_version = version.split('.')[0]
|
|
57
|
+
return {
|
|
58
|
+
'full_version': version,
|
|
59
|
+
'major_version': major_version
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def extract_package_version_from_package_json(content: str, package_name: str) -> Optional[Dict]:
|
|
66
|
+
"""
|
|
67
|
+
Extrae la versión de un paquete del contenido de package.json
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
content: Contenido del archivo package.json
|
|
71
|
+
package_name: Nombre del paquete a buscar (ej: '@bancolombia/design-system-web')
|
|
72
|
+
|
|
73
|
+
Returns:
|
|
74
|
+
Diccionario con versión completa y major, o None
|
|
75
|
+
"""
|
|
76
|
+
if not content:
|
|
77
|
+
return None
|
|
78
|
+
|
|
79
|
+
try:
|
|
80
|
+
# Intentar parsear como JSON
|
|
81
|
+
package_data = json.loads(content)
|
|
82
|
+
|
|
83
|
+
# Buscar en dependencies y devDependencies
|
|
84
|
+
for dep_key in ['dependencies', 'devDependencies']:
|
|
85
|
+
if dep_key in package_data:
|
|
86
|
+
deps = package_data[dep_key]
|
|
87
|
+
if package_name in deps:
|
|
88
|
+
version_str = deps[package_name]
|
|
89
|
+
|
|
90
|
+
# Extraer versión semántica (eliminar ^, ~, >=, etc.)
|
|
91
|
+
version_match = re.search(r'([0-9]+\.[0-9]+\.[0-9]+)', version_str)
|
|
92
|
+
if version_match:
|
|
93
|
+
version = version_match.group(1)
|
|
94
|
+
major_version = version.split('.')[0]
|
|
95
|
+
return {
|
|
96
|
+
'full_version': version,
|
|
97
|
+
'major_version': major_version
|
|
98
|
+
}
|
|
99
|
+
except json.JSONDecodeError:
|
|
100
|
+
# Si falla el parseo JSON, intentar con regex
|
|
101
|
+
pass
|
|
102
|
+
|
|
103
|
+
# Fallback: buscar con regex
|
|
104
|
+
pattern = rf'"{re.escape(package_name)}"\s*:\s*"[\^~>=<]*([0-9]+\.[0-9]+\.[0-9]+)'
|
|
105
|
+
match = re.search(pattern, content)
|
|
106
|
+
if match:
|
|
107
|
+
version = match.group(1)
|
|
108
|
+
major_version = version.split('.')[0]
|
|
109
|
+
return {
|
|
110
|
+
'full_version': version,
|
|
111
|
+
'major_version': major_version
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return None
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import psycopg2
|
|
2
|
+
|
|
3
|
+
class DatabaseConnection:
|
|
4
|
+
"""
|
|
5
|
+
Class to handle PostgreSQL database connections
|
|
6
|
+
using AWS Secrets Manager credentials
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
def __init__(self):
|
|
10
|
+
self.connection = None
|
|
11
|
+
|
|
12
|
+
def connect(self, credentials):
|
|
13
|
+
"""Establishes database connection using credentials"""
|
|
14
|
+
try:
|
|
15
|
+
self.connection = psycopg2.connect(
|
|
16
|
+
host=credentials['host'],
|
|
17
|
+
port=credentials['port'],
|
|
18
|
+
database=credentials['dbname'],
|
|
19
|
+
user=credentials['username'],
|
|
20
|
+
password=credentials['password'],
|
|
21
|
+
connect_timeout=30
|
|
22
|
+
)
|
|
23
|
+
except Exception as e:
|
|
24
|
+
raise Exception(f"Database connection error: {str(e)}")
|
|
25
|
+
|
|
26
|
+
def close(self):
|
|
27
|
+
"""Closes the database connection"""
|
|
28
|
+
if self.connection and not self.connection.closed:
|
|
29
|
+
self.connection.close()
|
|
30
|
+
|
|
31
|
+
def commit_transaction(self):
|
|
32
|
+
"""Commits pending transactions"""
|
|
33
|
+
if self.connection and not self.connection.closed:
|
|
34
|
+
self.connection.commit()
|
|
35
|
+
|
|
36
|
+
def rollback_transaction(self):
|
|
37
|
+
"""Rolls back pending transactions in case of error"""
|
|
38
|
+
if self.connection and not self.connection.closed:
|
|
39
|
+
self.connection.rollback()
|