commons-metrics 0.0.23__py3-none-any.whl → 0.0.25__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 +1 -1
- commons_metrics/github_api_client.py +282 -99
- {commons_metrics-0.0.23.dist-info → commons_metrics-0.0.25.dist-info}/METADATA +1 -1
- {commons_metrics-0.0.23.dist-info → commons_metrics-0.0.25.dist-info}/RECORD +7 -7
- {commons_metrics-0.0.23.dist-info → commons_metrics-0.0.25.dist-info}/WHEEL +0 -0
- {commons_metrics-0.0.23.dist-info → commons_metrics-0.0.25.dist-info}/licenses/LICENSE +0 -0
- {commons_metrics-0.0.23.dist-info → commons_metrics-0.0.25.dist-info}/top_level.txt +0 -0
commons_metrics/__init__.py
CHANGED
|
@@ -12,4 +12,4 @@ from .text_simplifier import TextSimplifier
|
|
|
12
12
|
from .variable_finder import VariableFinder
|
|
13
13
|
|
|
14
14
|
__all__ = ['Util', 'DatabaseConnection', 'ComponentRepository', 'UpdateDesignSystemComponents', 'GitHubAPIClient', 'AzureDevOpsClient', 'S3FileManager', 'CacheManager', 'CommonsReposClient', 'DateUtils', 'TextSimplifier', 'VariableFinder']
|
|
15
|
-
__version__ = '0.0.
|
|
15
|
+
__version__ = '0.0.25'
|
|
@@ -1,65 +1,242 @@
|
|
|
1
1
|
import requests
|
|
2
2
|
import base64
|
|
3
|
+
import time
|
|
4
|
+
from threading import Lock
|
|
3
5
|
from typing import List, Dict, Optional
|
|
4
6
|
import os
|
|
5
7
|
from .commons_repos_client import CommonsReposClient
|
|
6
8
|
|
|
9
|
+
|
|
10
|
+
class RateLimiter:
|
|
11
|
+
"""
|
|
12
|
+
Controla el rate limiting para respetar los límites de API de GitHub.
|
|
13
|
+
GitHub permite 5000 requests/hora autenticados = ~1.4 TPS.
|
|
14
|
+
|
|
15
|
+
Estrategia adaptativa:
|
|
16
|
+
1. Cuando está cerca del límite (>90%), aumenta delays si falta >10min para reset
|
|
17
|
+
2. Si faltan <10min para reset, reduce delay a 10ms para usar toda la cuota
|
|
18
|
+
3. Exponential backoff solo para errores 403/429 de rate limit
|
|
19
|
+
"""
|
|
20
|
+
def __init__(self, delay: float = 0.72, token: str = None):
|
|
21
|
+
self.base_delay = delay
|
|
22
|
+
self.current_delay = delay
|
|
23
|
+
self.last_request_time = 0
|
|
24
|
+
self.lock = Lock()
|
|
25
|
+
self.token = token
|
|
26
|
+
self.last_rate_check = 0
|
|
27
|
+
self.rate_check_interval = 10 # Verificar rate limit cada 10 requests
|
|
28
|
+
self.request_count = 0
|
|
29
|
+
# Variables globales para rate limit
|
|
30
|
+
self.remaining_requests = None
|
|
31
|
+
self.limit = None
|
|
32
|
+
self.reset_time = None
|
|
33
|
+
|
|
34
|
+
def check_rate_limit(self):
|
|
35
|
+
"""Obtiene el estado actual del rate limit desde GitHub y actualiza variables globales"""
|
|
36
|
+
if not self.token:
|
|
37
|
+
return None, None, None
|
|
38
|
+
|
|
39
|
+
try:
|
|
40
|
+
response = requests.get(
|
|
41
|
+
"https://api.github.com/rate_limit",
|
|
42
|
+
headers={"Authorization": f"token {self.token}"}
|
|
43
|
+
)
|
|
44
|
+
if response.status_code == 200:
|
|
45
|
+
data = response.json()
|
|
46
|
+
self.remaining_requests = data['rate']['remaining']
|
|
47
|
+
self.limit = data['rate']['limit']
|
|
48
|
+
self.reset_time = data['rate']['reset']
|
|
49
|
+
return self.remaining_requests, self.limit, self.reset_time
|
|
50
|
+
except Exception as e:
|
|
51
|
+
print(f"Warning: No se pudo verificar rate limit: {e}")
|
|
52
|
+
|
|
53
|
+
return None, None, None
|
|
54
|
+
|
|
55
|
+
def adjust_delay(self):
|
|
56
|
+
"""
|
|
57
|
+
Ajusta el delay dinámicamente basado en el rate limit actual:
|
|
58
|
+
- >90% usado y falta >10min: aumentar delay
|
|
59
|
+
- <10min para reset: reducir delay a 10ms
|
|
60
|
+
"""
|
|
61
|
+
remaining, limit, reset_time = self.check_rate_limit()
|
|
62
|
+
|
|
63
|
+
if remaining is None or limit is None or reset_time is None:
|
|
64
|
+
return
|
|
65
|
+
|
|
66
|
+
usage_percent = ((limit - remaining) / limit) * 100
|
|
67
|
+
time_to_reset = reset_time - time.time()
|
|
68
|
+
minutes_to_reset = time_to_reset / 60
|
|
69
|
+
|
|
70
|
+
# Estrategia 1: Cerca del límite (>90%) y falta >10min
|
|
71
|
+
if usage_percent > 90 and minutes_to_reset > 10:
|
|
72
|
+
# Aumentar delay progresivamente
|
|
73
|
+
self.current_delay = self.base_delay * 2
|
|
74
|
+
print(f"⚠️ Rate limit alto ({usage_percent:.1f}%). Aumentando delay a {self.current_delay:.2f}s")
|
|
75
|
+
|
|
76
|
+
# Estrategia 2: Faltan <10min para reset
|
|
77
|
+
elif minutes_to_reset < 10:
|
|
78
|
+
# Reducir delay para aprovechar requests restantes
|
|
79
|
+
self.current_delay = 0.01 # 10ms
|
|
80
|
+
print(f"🚀 Quedan {minutes_to_reset:.1f}min para reset. Acelerando a {self.current_delay*1000:.0f}ms")
|
|
81
|
+
|
|
82
|
+
# Normal: restaurar delay base
|
|
83
|
+
else:
|
|
84
|
+
self.current_delay = self.base_delay
|
|
85
|
+
|
|
86
|
+
def wait(self):
|
|
87
|
+
"""Espera el tiempo necesario antes de hacer el siguiente request"""
|
|
88
|
+
with self.lock:
|
|
89
|
+
# Verificar y ajustar delay cada N requests
|
|
90
|
+
self.request_count += 1
|
|
91
|
+
if self.request_count % self.rate_check_interval == 0:
|
|
92
|
+
self.adjust_delay()
|
|
93
|
+
|
|
94
|
+
current_time = time.time()
|
|
95
|
+
time_since_last_request = current_time - self.last_request_time
|
|
96
|
+
|
|
97
|
+
if time_since_last_request < self.current_delay:
|
|
98
|
+
sleep_time = self.current_delay - time_since_last_request
|
|
99
|
+
time.sleep(sleep_time)
|
|
100
|
+
|
|
101
|
+
self.last_request_time = time.time()
|
|
102
|
+
|
|
103
|
+
|
|
7
104
|
class GitHubAPIClient:
|
|
8
105
|
"""
|
|
9
|
-
Cliente para interactuar con la API de GitHub
|
|
106
|
+
Cliente para interactuar con la API de GitHub con rate limiting integrado.
|
|
107
|
+
Controla automáticamente el TPS para no exceder los límites de GitHub (5000 req/hora).
|
|
108
|
+
|
|
109
|
+
Incluye:
|
|
110
|
+
- Rate limiting adaptativo (ajusta delays según uso)
|
|
111
|
+
- Exponential backoff para errores 403/429
|
|
112
|
+
- Optimización de requests en últimos 10 minutos antes de reset
|
|
10
113
|
"""
|
|
11
|
-
|
|
12
|
-
|
|
114
|
+
|
|
115
|
+
# Rate limiter compartido entre todas las instancias
|
|
116
|
+
_rate_limiter = None
|
|
117
|
+
|
|
118
|
+
def __init__(self, token: str, owner: str = None, repo: str = None, enable_rate_limit: bool = True):
|
|
13
119
|
"""
|
|
14
120
|
Inicializa el cliente de GitHub API
|
|
15
|
-
|
|
121
|
+
|
|
16
122
|
Args:
|
|
17
123
|
token: Personal Access Token de GitHub
|
|
18
124
|
owner: Dueño del repositorio (ej: 'grupobancolombia-innersource') - Opcional
|
|
19
125
|
repo: Nombre del repositorio (ej: 'NU0066001_BDS_MOBILE_Lib') - Opcional
|
|
126
|
+
enable_rate_limit: Si True, aplica rate limiting automático (por defecto True)
|
|
20
127
|
"""
|
|
21
128
|
self.token = token
|
|
22
129
|
self.owner = owner
|
|
23
130
|
self.repo = repo
|
|
131
|
+
self.enable_rate_limit = enable_rate_limit
|
|
24
132
|
self.base_url = f"https://api.github.com/repos/{owner}/{repo}" if owner and repo else "https://api.github.com"
|
|
25
133
|
self.headers = {
|
|
26
134
|
"Authorization": f"token {token}",
|
|
27
135
|
"Accept": "application/vnd.github.v3+json"
|
|
28
136
|
}
|
|
29
|
-
|
|
137
|
+
|
|
138
|
+
# Inicializar rate limiter compartido con token
|
|
139
|
+
if GitHubAPIClient._rate_limiter is None:
|
|
140
|
+
GitHubAPIClient._rate_limiter = RateLimiter(delay=0.72, token=token)
|
|
141
|
+
|
|
142
|
+
def _request_with_backoff(self, method: str, url: str, max_retries: int = 5, **kwargs):
|
|
143
|
+
"""
|
|
144
|
+
Hace un request con exponential backoff SOLO para errores de rate limit (403/429)
|
|
145
|
+
|
|
146
|
+
Estrategia:
|
|
147
|
+
- Si hay X-RateLimit-Reset header, espera hasta ese momento
|
|
148
|
+
- Si no, usa exponential backoff: 2^attempt segundos (1s, 2s, 4s, 8s, 16s)
|
|
149
|
+
- Máximo 5 reintentos
|
|
150
|
+
|
|
151
|
+
Args:
|
|
152
|
+
method: Método HTTP ('get', 'post', etc)
|
|
153
|
+
url: URL del endpoint
|
|
154
|
+
max_retries: Número máximo de reintentos (default: 5)
|
|
155
|
+
**kwargs: Argumentos adicionales para requests
|
|
156
|
+
|
|
157
|
+
Returns:
|
|
158
|
+
Response object
|
|
159
|
+
|
|
160
|
+
Raises:
|
|
161
|
+
Exception: Si no es error de rate limit o se agotan los reintentos
|
|
162
|
+
"""
|
|
163
|
+
request_func = getattr(requests, method.lower())
|
|
164
|
+
|
|
165
|
+
for attempt in range(max_retries):
|
|
166
|
+
if self.enable_rate_limit:
|
|
167
|
+
self._rate_limiter.wait()
|
|
168
|
+
|
|
169
|
+
response = request_func(url, **kwargs)
|
|
170
|
+
|
|
171
|
+
# Verificar si es específicamente un error de rate limit
|
|
172
|
+
if response.status_code in [403, 429]:
|
|
173
|
+
# Verificar que sea rate limit y no otro error 403
|
|
174
|
+
if 'rate limit' in response.text.lower() or response.status_code == 429:
|
|
175
|
+
if attempt < max_retries - 1:
|
|
176
|
+
# Usar el reset_time global del RateLimiter
|
|
177
|
+
reset_time = self._rate_limiter.reset_time
|
|
178
|
+
|
|
179
|
+
if reset_time:
|
|
180
|
+
# Esperar hasta el reset (con 5 segundos extra de margen)
|
|
181
|
+
wait_time = reset_time - time.time() + 5
|
|
182
|
+
if wait_time > 0:
|
|
183
|
+
wait_minutes = wait_time / 60
|
|
184
|
+
print(f"⚠️ Rate limit alcanzado. Esperando {wait_minutes:.1f} minutos hasta reset...")
|
|
185
|
+
time.sleep(wait_time)
|
|
186
|
+
# Verificar rate limit actualizado después de esperar
|
|
187
|
+
self._rate_limiter.check_rate_limit()
|
|
188
|
+
continue
|
|
189
|
+
else:
|
|
190
|
+
# Si no hay reset_time global, usar exponential backoff
|
|
191
|
+
# 1s, 2s, 4s, 8s, 16s (máx 60s)
|
|
192
|
+
wait_time = min(2 ** attempt, 60)
|
|
193
|
+
print(f"⚠️ Rate limit alcanzado. Esperando {wait_time}s (intento {attempt + 1}/{max_retries})...")
|
|
194
|
+
time.sleep(wait_time)
|
|
195
|
+
continue
|
|
196
|
+
else:
|
|
197
|
+
raise Exception(f"Rate limit excedido después de {max_retries} intentos")
|
|
198
|
+
else:
|
|
199
|
+
# Es un 403 pero NO de rate limit - no reintentar
|
|
200
|
+
raise Exception(f"Error 403 (no rate limit): {response.text}")
|
|
201
|
+
|
|
202
|
+
# Si es exitoso o cualquier otro error, retornar
|
|
203
|
+
return response
|
|
204
|
+
|
|
205
|
+
return response
|
|
206
|
+
|
|
30
207
|
def get_directory_contents(self, path: str = "") -> List[Dict]:
|
|
31
208
|
"""
|
|
32
209
|
Obtiene el contenido de un directorio en el repositorio
|
|
33
|
-
|
|
210
|
+
|
|
34
211
|
Args:
|
|
35
212
|
path: Ruta del directorio (ej: 'lib/atoms')
|
|
36
|
-
|
|
213
|
+
|
|
37
214
|
Returns:
|
|
38
215
|
Lista de diccionarios con información de archivos/carpetas
|
|
39
216
|
"""
|
|
40
217
|
url = f"{self.base_url}/contents/{path}"
|
|
41
|
-
response =
|
|
42
|
-
|
|
218
|
+
response = self._request_with_backoff('get', url, headers=self.headers)
|
|
219
|
+
|
|
43
220
|
if response.status_code == 200:
|
|
44
221
|
return response.json()
|
|
45
222
|
elif response.status_code == 404:
|
|
46
223
|
return []
|
|
47
224
|
else:
|
|
48
225
|
raise Exception(f"Error getting directory contents: {response.status_code} - {response.text}")
|
|
49
|
-
|
|
226
|
+
|
|
50
227
|
def get_file_content(self, path: str) -> Optional[str]:
|
|
51
228
|
"""
|
|
52
229
|
Obtiene el contenido de un archivo
|
|
53
|
-
|
|
230
|
+
|
|
54
231
|
Args:
|
|
55
232
|
path: Ruta del archivo en el repositorio
|
|
56
|
-
|
|
233
|
+
|
|
57
234
|
Returns:
|
|
58
235
|
Contenido del archivo como string, o None si no existe
|
|
59
236
|
"""
|
|
60
237
|
url = f"{self.base_url}/contents/{path}"
|
|
61
|
-
response =
|
|
62
|
-
|
|
238
|
+
response = self._request_with_backoff('get', url, headers=self.headers)
|
|
239
|
+
|
|
63
240
|
if response.status_code == 200:
|
|
64
241
|
content = response.json()
|
|
65
242
|
if content.get('encoding') == 'base64':
|
|
@@ -70,85 +247,85 @@ class GitHubAPIClient:
|
|
|
70
247
|
return None
|
|
71
248
|
else:
|
|
72
249
|
raise Exception(f"Error getting file content: {response.status_code} - {response.text}")
|
|
73
|
-
|
|
250
|
+
|
|
74
251
|
def list_folders_in_directory(self, path: str) -> List[str]:
|
|
75
252
|
"""
|
|
76
253
|
Lista solo las carpetas dentro de un directorio
|
|
77
|
-
|
|
254
|
+
|
|
78
255
|
Args:
|
|
79
256
|
path: Ruta del directorio
|
|
80
|
-
|
|
257
|
+
|
|
81
258
|
Returns:
|
|
82
259
|
Lista de nombres de carpetas
|
|
83
260
|
"""
|
|
84
261
|
contents = self.get_directory_contents(path)
|
|
85
262
|
folders = [
|
|
86
|
-
item['name']
|
|
87
|
-
for item in contents
|
|
263
|
+
item['name']
|
|
264
|
+
for item in contents
|
|
88
265
|
if item['type'] == 'dir'
|
|
89
266
|
]
|
|
90
267
|
return folders
|
|
91
|
-
|
|
268
|
+
|
|
92
269
|
def walk_directory(self, path: str = "", extension: str = None, exclude_patterns: List[str] = None) -> List[Dict]:
|
|
93
270
|
"""
|
|
94
271
|
Recorre recursivamente un directorio y retorna todos los archivos
|
|
95
|
-
|
|
272
|
+
|
|
96
273
|
Args:
|
|
97
274
|
path: Ruta del directorio inicial
|
|
98
275
|
extension: Extensión a filtrar (ej: '.ts', '.dart')
|
|
99
276
|
exclude_patterns: Lista de patrones a excluir (ej: ['.spec.', '.test.', '.d.ts'])
|
|
100
|
-
|
|
277
|
+
|
|
101
278
|
Returns:
|
|
102
279
|
Lista de diccionarios con información de archivos encontrados
|
|
103
280
|
"""
|
|
104
281
|
all_files = []
|
|
105
282
|
exclude_patterns = exclude_patterns or []
|
|
106
|
-
|
|
283
|
+
|
|
107
284
|
def should_exclude(filename: str) -> bool:
|
|
108
285
|
return any(pattern in filename for pattern in exclude_patterns)
|
|
109
|
-
|
|
286
|
+
|
|
110
287
|
def recurse_directory(current_path: str):
|
|
111
288
|
contents = self.get_directory_contents(current_path)
|
|
112
|
-
|
|
289
|
+
|
|
113
290
|
for item in contents:
|
|
114
291
|
item_path = f"{current_path}/{item['name']}" if current_path else item['name']
|
|
115
|
-
|
|
292
|
+
|
|
116
293
|
if item['type'] == 'file':
|
|
117
294
|
# Aplicar filtros de extensión y exclusión
|
|
118
295
|
if extension and not item['name'].endswith(extension):
|
|
119
296
|
continue
|
|
120
297
|
if should_exclude(item['name']):
|
|
121
298
|
continue
|
|
122
|
-
|
|
299
|
+
|
|
123
300
|
all_files.append({
|
|
124
301
|
'name': item['name'],
|
|
125
302
|
'path': item_path,
|
|
126
303
|
'url': item.get('url', ''),
|
|
127
304
|
'download_url': item.get('download_url', '')
|
|
128
305
|
})
|
|
129
|
-
|
|
306
|
+
|
|
130
307
|
elif item['type'] == 'dir':
|
|
131
308
|
# Excluir directorios comunes que no contienen componentes
|
|
132
309
|
if item['name'] not in ['node_modules', 'dist', 'build', '.git', 'test', 'tests', '__pycache__']:
|
|
133
310
|
recurse_directory(item_path)
|
|
134
|
-
|
|
311
|
+
|
|
135
312
|
recurse_directory(path)
|
|
136
313
|
return all_files
|
|
137
|
-
|
|
314
|
+
|
|
138
315
|
def search_code(self, query: str, per_page: int = 100) -> List[Dict]:
|
|
139
316
|
"""
|
|
140
317
|
Busca código en GitHub usando la API de búsqueda
|
|
141
|
-
|
|
318
|
+
|
|
142
319
|
Args:
|
|
143
320
|
query: Query de búsqueda (ej: '"bds_mobile" in:file filename:pubspec.yaml')
|
|
144
321
|
per_page: Número de resultados por página (máximo 100)
|
|
145
|
-
|
|
322
|
+
|
|
146
323
|
Returns:
|
|
147
324
|
Lista de diccionarios con información de archivos encontrados
|
|
148
325
|
"""
|
|
149
326
|
all_results = []
|
|
150
327
|
page = 1
|
|
151
|
-
|
|
328
|
+
|
|
152
329
|
while True:
|
|
153
330
|
url = "https://api.github.com/search/code"
|
|
154
331
|
params = {
|
|
@@ -156,53 +333,54 @@ class GitHubAPIClient:
|
|
|
156
333
|
'per_page': per_page,
|
|
157
334
|
'page': page
|
|
158
335
|
}
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
336
|
+
|
|
337
|
+
try:
|
|
338
|
+
response = self._request_with_backoff('get', url, headers=self.headers, params=params)
|
|
339
|
+
|
|
340
|
+
if response.status_code == 200:
|
|
341
|
+
data = response.json()
|
|
342
|
+
items = data.get('items', [])
|
|
343
|
+
|
|
344
|
+
if not items:
|
|
345
|
+
break
|
|
346
|
+
|
|
347
|
+
all_results.extend(items)
|
|
348
|
+
|
|
349
|
+
# Si hay menos items que per_page, es la última página
|
|
350
|
+
if len(items) < per_page:
|
|
351
|
+
break
|
|
352
|
+
|
|
353
|
+
page += 1
|
|
354
|
+
else:
|
|
355
|
+
raise Exception(f"Error searching code: {response.status_code} - {response.text}")
|
|
356
|
+
|
|
357
|
+
except Exception as e:
|
|
358
|
+
print(f"⚠️ Error en búsqueda: {e}. Resultados obtenidos: {len(all_results)}")
|
|
179
359
|
break
|
|
180
|
-
|
|
181
|
-
raise Exception(f"Error searching code: {response.status_code} - {response.text}")
|
|
182
|
-
|
|
360
|
+
|
|
183
361
|
return all_results
|
|
184
|
-
|
|
362
|
+
|
|
185
363
|
def search_projects_with_bds(self, platform: str, design_system_name: str = None) -> List[Dict]:
|
|
186
364
|
"""
|
|
187
365
|
Busca proyectos que usan el sistema de diseño BDS (mobile o web)
|
|
188
|
-
|
|
366
|
+
|
|
189
367
|
Args:
|
|
190
368
|
platform: 'mobile' o 'web'
|
|
191
369
|
design_system_name: Nombre del paquete del sistema de diseño
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
370
|
+
Si no se proporciona, usa valores por defecto:
|
|
371
|
+
- mobile: "bds_mobile"
|
|
372
|
+
- web: "@bancolombia/design-system-web"
|
|
373
|
+
|
|
196
374
|
Returns:
|
|
197
375
|
Lista de proyectos con información del repositorio incluyendo la versión
|
|
198
|
-
|
|
376
|
+
|
|
199
377
|
Raises:
|
|
200
378
|
ValueError: Si platform no es 'mobile' o 'web'
|
|
201
379
|
"""
|
|
202
380
|
# Validar plataforma
|
|
203
381
|
if platform not in ['mobile', 'web']:
|
|
204
382
|
raise ValueError(f"Platform debe ser 'mobile' o 'web', se recibió: {platform}")
|
|
205
|
-
|
|
383
|
+
|
|
206
384
|
# Configurar valores según la plataforma
|
|
207
385
|
if platform == 'mobile':
|
|
208
386
|
config_file = 'pubspec.yaml'
|
|
@@ -212,21 +390,21 @@ class GitHubAPIClient:
|
|
|
212
390
|
config_file = 'package.json'
|
|
213
391
|
default_package_name = '@bancolombia/design-system-web'
|
|
214
392
|
extract_version_method = CommonsReposClient.extract_package_version_from_package_json
|
|
215
|
-
|
|
393
|
+
|
|
216
394
|
# Usar nombre de paquete por defecto si no se proporciona
|
|
217
395
|
package_name = design_system_name or default_package_name
|
|
218
|
-
|
|
396
|
+
|
|
219
397
|
# Agregar filtro de organización si self.owner está definido
|
|
220
398
|
org_filter = f" org:{self.owner}" if self.owner else ""
|
|
221
399
|
query = f'"{package_name}" in:file filename:{config_file}{org_filter}'
|
|
222
400
|
results = self.search_code(query)
|
|
223
|
-
|
|
401
|
+
|
|
224
402
|
projects = []
|
|
225
403
|
for item in results:
|
|
226
404
|
# Obtener contenido del archivo de configuración para extraer la versión
|
|
227
405
|
file_content = self._get_file_content_from_url(item['url'])
|
|
228
406
|
version = extract_version_method(file_content, package_name)
|
|
229
|
-
|
|
407
|
+
|
|
230
408
|
project_info = {
|
|
231
409
|
'name': item['repository']['name'],
|
|
232
410
|
'full_name': item['repository']['full_name'],
|
|
@@ -236,66 +414,71 @@ class GitHubAPIClient:
|
|
|
236
414
|
'bds_version': version
|
|
237
415
|
}
|
|
238
416
|
projects.append(project_info)
|
|
239
|
-
|
|
417
|
+
|
|
240
418
|
return projects
|
|
241
|
-
|
|
419
|
+
|
|
242
420
|
def search_repositories(self, query: str, per_page: int = 100) -> List[Dict]:
|
|
243
421
|
"""
|
|
244
422
|
Busca repositorios en GitHub usando la API de búsqueda
|
|
245
|
-
|
|
423
|
+
|
|
246
424
|
Args:
|
|
247
425
|
query: Query de búsqueda (ej: 'NU0296001 mobile in:name org:grupobancolombia-innersource')
|
|
248
426
|
per_page: Número de resultados por página (máximo 100)
|
|
249
|
-
|
|
427
|
+
|
|
250
428
|
Returns:
|
|
251
429
|
Lista de diccionarios con información de repositorios encontrados
|
|
252
430
|
"""
|
|
253
431
|
all_results = []
|
|
254
432
|
page = 1
|
|
255
433
|
search_url = "https://api.github.com/search/repositories"
|
|
256
|
-
|
|
434
|
+
|
|
257
435
|
while True:
|
|
258
436
|
params = {
|
|
259
437
|
'q': query,
|
|
260
438
|
'per_page': per_page,
|
|
261
439
|
'page': page
|
|
262
440
|
}
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
441
|
+
|
|
442
|
+
try:
|
|
443
|
+
response = self._request_with_backoff('get', search_url, headers=self.headers, params=params)
|
|
444
|
+
|
|
445
|
+
if response.status_code != 200:
|
|
446
|
+
print(f"Error searching repositories: {response.status_code}")
|
|
447
|
+
print(f"Response: {response.text}")
|
|
448
|
+
break
|
|
449
|
+
|
|
450
|
+
data = response.json()
|
|
451
|
+
items = data.get('items', [])
|
|
452
|
+
|
|
453
|
+
if not items:
|
|
454
|
+
break
|
|
455
|
+
|
|
456
|
+
all_results.extend(items)
|
|
457
|
+
|
|
458
|
+
# Verificar si hay más páginas
|
|
459
|
+
if len(items) < per_page:
|
|
460
|
+
break
|
|
461
|
+
|
|
462
|
+
page += 1
|
|
463
|
+
|
|
464
|
+
except Exception as e:
|
|
465
|
+
print(f"⚠️ Error buscando repositorios: {e}")
|
|
281
466
|
break
|
|
282
|
-
|
|
283
|
-
page += 1
|
|
284
|
-
|
|
467
|
+
|
|
285
468
|
return all_results
|
|
286
|
-
|
|
469
|
+
|
|
287
470
|
def _get_file_content_from_url(self, api_url: str) -> Optional[str]:
|
|
288
471
|
"""
|
|
289
472
|
Obtiene el contenido de un archivo desde una URL de la API de GitHub
|
|
290
|
-
|
|
473
|
+
|
|
291
474
|
Args:
|
|
292
475
|
api_url: URL de la API de GitHub para el archivo
|
|
293
|
-
|
|
476
|
+
|
|
294
477
|
Returns:
|
|
295
478
|
Contenido del archivo como string, o None si no existe
|
|
296
479
|
"""
|
|
297
|
-
response =
|
|
298
|
-
|
|
480
|
+
response = self._request_with_backoff('get', api_url, headers=self.headers)
|
|
481
|
+
|
|
299
482
|
if response.status_code == 200:
|
|
300
483
|
content = response.json()
|
|
301
484
|
if content.get('encoding') == 'base64':
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
commons_metrics/__init__.py,sha256=
|
|
1
|
+
commons_metrics/__init__.py,sha256=Kge7GuDHOjv5eD6u5y1BDUs27R3Kzn65hj3dFYq9pM0,796
|
|
2
2
|
commons_metrics/azure_devops_client.py,sha256=XrGqhr4voFUtGkdB27tnzsMSiNdpfY4stQUU51e1a0U,16755
|
|
3
3
|
commons_metrics/cache_manager.py,sha256=HOeup9twUizjJAbh1MNXdPT8BMVeLFoolOWlAzMTXkE,2651
|
|
4
4
|
commons_metrics/commons_repos_client.py,sha256=PiAMLWuDnI8AlZzE3sfQ3s2P23UrYbbqaq63AFRroHc,4695
|
|
5
5
|
commons_metrics/database.py,sha256=wzcrK8q09J28NMLExkhWy8w7N2fQPkckrGf62C9rdQ0,1989
|
|
6
6
|
commons_metrics/date_utils.py,sha256=8465712QJDGcshqry97Gi90lbMEbvbX3uiuHRVwGHbE,2654
|
|
7
|
-
commons_metrics/github_api_client.py,sha256=
|
|
7
|
+
commons_metrics/github_api_client.py,sha256=as6VePgfAn1zKBGi_1piPqbf3X7AJzwQ-InCjlwHnxs,18710
|
|
8
8
|
commons_metrics/repositories.py,sha256=47JK9rhcpx_X6RWRkM3768qTwk39ODm8LLvi6QzZDdQ,9480
|
|
9
9
|
commons_metrics/s3_file_manager.py,sha256=Mm7vlPJeXB46LnCXFs9oz7PQezcIcgYLlSJqMzVf72g,3384
|
|
10
10
|
commons_metrics/text_simplifier.py,sha256=jRYckQ5APR0A3wY4hg70kOHxHUFuGzd6D-CJC3__j78,1518
|
|
11
11
|
commons_metrics/update_design_components.py,sha256=QpY0GCCCMjdYOZ7b8oNigU9iTpiGx91CYsyWwN8WVDA,7660
|
|
12
12
|
commons_metrics/util.py,sha256=98zuynalXumQRh-BB0Bcjyoh6vS2BTOUM8tVgr7iS9Q,1225
|
|
13
13
|
commons_metrics/variable_finder.py,sha256=pxI_XSd-lq_AiUjDbcUC4knIZhWZwt7HQrQOa-F0ud4,8061
|
|
14
|
-
commons_metrics-0.0.
|
|
15
|
-
commons_metrics-0.0.
|
|
16
|
-
commons_metrics-0.0.
|
|
17
|
-
commons_metrics-0.0.
|
|
18
|
-
commons_metrics-0.0.
|
|
14
|
+
commons_metrics-0.0.25.dist-info/licenses/LICENSE,sha256=jsHZ2Sh1wCL74HC25pDDGXCyQ0xgsTAy62FvEnehKIg,1067
|
|
15
|
+
commons_metrics-0.0.25.dist-info/METADATA,sha256=Kc29zW719F2lFCX16tN6RTLIzyexfPvYJ7LJMT6F7ag,402
|
|
16
|
+
commons_metrics-0.0.25.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
17
|
+
commons_metrics-0.0.25.dist-info/top_level.txt,sha256=lheUN-3OKdU3A8Tg8Y-1IEB_9i_vVRA0g_FOiUsTQz8,16
|
|
18
|
+
commons_metrics-0.0.25.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|