commons-metrics 0.0.23__tar.gz → 0.0.25__tar.gz
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-0.0.23 → commons_metrics-0.0.25}/PKG-INFO +1 -1
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/__init__.py +1 -1
- commons_metrics-0.0.25/commons_metrics/github_api_client.py +488 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/PKG-INFO +1 -1
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/setup.py +1 -1
- commons_metrics-0.0.23/commons_metrics/github_api_client.py +0 -305
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/LICENSE +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/README.md +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/azure_devops_client.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/cache_manager.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/commons_repos_client.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/database.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/date_utils.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/repositories.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/s3_file_manager.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/text_simplifier.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/update_design_components.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/util.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/variable_finder.py +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/SOURCES.txt +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/dependency_links.txt +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/requires.txt +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/top_level.txt +0 -0
- {commons_metrics-0.0.23 → commons_metrics-0.0.25}/setup.cfg +0 -0
|
@@ -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'
|
|
@@ -0,0 +1,488 @@
|
|
|
1
|
+
import requests
|
|
2
|
+
import base64
|
|
3
|
+
import time
|
|
4
|
+
from threading import Lock
|
|
5
|
+
from typing import List, Dict, Optional
|
|
6
|
+
import os
|
|
7
|
+
from .commons_repos_client import CommonsReposClient
|
|
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
|
+
|
|
104
|
+
class GitHubAPIClient:
|
|
105
|
+
"""
|
|
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
|
|
113
|
+
"""
|
|
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):
|
|
119
|
+
"""
|
|
120
|
+
Inicializa el cliente de GitHub API
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
token: Personal Access Token de GitHub
|
|
124
|
+
owner: Dueño del repositorio (ej: 'grupobancolombia-innersource') - Opcional
|
|
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)
|
|
127
|
+
"""
|
|
128
|
+
self.token = token
|
|
129
|
+
self.owner = owner
|
|
130
|
+
self.repo = repo
|
|
131
|
+
self.enable_rate_limit = enable_rate_limit
|
|
132
|
+
self.base_url = f"https://api.github.com/repos/{owner}/{repo}" if owner and repo else "https://api.github.com"
|
|
133
|
+
self.headers = {
|
|
134
|
+
"Authorization": f"token {token}",
|
|
135
|
+
"Accept": "application/vnd.github.v3+json"
|
|
136
|
+
}
|
|
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
|
+
|
|
207
|
+
def get_directory_contents(self, path: str = "") -> List[Dict]:
|
|
208
|
+
"""
|
|
209
|
+
Obtiene el contenido de un directorio en el repositorio
|
|
210
|
+
|
|
211
|
+
Args:
|
|
212
|
+
path: Ruta del directorio (ej: 'lib/atoms')
|
|
213
|
+
|
|
214
|
+
Returns:
|
|
215
|
+
Lista de diccionarios con información de archivos/carpetas
|
|
216
|
+
"""
|
|
217
|
+
url = f"{self.base_url}/contents/{path}"
|
|
218
|
+
response = self._request_with_backoff('get', url, headers=self.headers)
|
|
219
|
+
|
|
220
|
+
if response.status_code == 200:
|
|
221
|
+
return response.json()
|
|
222
|
+
elif response.status_code == 404:
|
|
223
|
+
return []
|
|
224
|
+
else:
|
|
225
|
+
raise Exception(f"Error getting directory contents: {response.status_code} - {response.text}")
|
|
226
|
+
|
|
227
|
+
def get_file_content(self, path: str) -> Optional[str]:
|
|
228
|
+
"""
|
|
229
|
+
Obtiene el contenido de un archivo
|
|
230
|
+
|
|
231
|
+
Args:
|
|
232
|
+
path: Ruta del archivo en el repositorio
|
|
233
|
+
|
|
234
|
+
Returns:
|
|
235
|
+
Contenido del archivo como string, o None si no existe
|
|
236
|
+
"""
|
|
237
|
+
url = f"{self.base_url}/contents/{path}"
|
|
238
|
+
response = self._request_with_backoff('get', url, headers=self.headers)
|
|
239
|
+
|
|
240
|
+
if response.status_code == 200:
|
|
241
|
+
content = response.json()
|
|
242
|
+
if content.get('encoding') == 'base64':
|
|
243
|
+
decoded_content = base64.b64decode(content['content']).decode('utf-8')
|
|
244
|
+
return decoded_content
|
|
245
|
+
return None
|
|
246
|
+
elif response.status_code == 404:
|
|
247
|
+
return None
|
|
248
|
+
else:
|
|
249
|
+
raise Exception(f"Error getting file content: {response.status_code} - {response.text}")
|
|
250
|
+
|
|
251
|
+
def list_folders_in_directory(self, path: str) -> List[str]:
|
|
252
|
+
"""
|
|
253
|
+
Lista solo las carpetas dentro de un directorio
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
path: Ruta del directorio
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
Lista de nombres de carpetas
|
|
260
|
+
"""
|
|
261
|
+
contents = self.get_directory_contents(path)
|
|
262
|
+
folders = [
|
|
263
|
+
item['name']
|
|
264
|
+
for item in contents
|
|
265
|
+
if item['type'] == 'dir'
|
|
266
|
+
]
|
|
267
|
+
return folders
|
|
268
|
+
|
|
269
|
+
def walk_directory(self, path: str = "", extension: str = None, exclude_patterns: List[str] = None) -> List[Dict]:
|
|
270
|
+
"""
|
|
271
|
+
Recorre recursivamente un directorio y retorna todos los archivos
|
|
272
|
+
|
|
273
|
+
Args:
|
|
274
|
+
path: Ruta del directorio inicial
|
|
275
|
+
extension: Extensión a filtrar (ej: '.ts', '.dart')
|
|
276
|
+
exclude_patterns: Lista de patrones a excluir (ej: ['.spec.', '.test.', '.d.ts'])
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Lista de diccionarios con información de archivos encontrados
|
|
280
|
+
"""
|
|
281
|
+
all_files = []
|
|
282
|
+
exclude_patterns = exclude_patterns or []
|
|
283
|
+
|
|
284
|
+
def should_exclude(filename: str) -> bool:
|
|
285
|
+
return any(pattern in filename for pattern in exclude_patterns)
|
|
286
|
+
|
|
287
|
+
def recurse_directory(current_path: str):
|
|
288
|
+
contents = self.get_directory_contents(current_path)
|
|
289
|
+
|
|
290
|
+
for item in contents:
|
|
291
|
+
item_path = f"{current_path}/{item['name']}" if current_path else item['name']
|
|
292
|
+
|
|
293
|
+
if item['type'] == 'file':
|
|
294
|
+
# Aplicar filtros de extensión y exclusión
|
|
295
|
+
if extension and not item['name'].endswith(extension):
|
|
296
|
+
continue
|
|
297
|
+
if should_exclude(item['name']):
|
|
298
|
+
continue
|
|
299
|
+
|
|
300
|
+
all_files.append({
|
|
301
|
+
'name': item['name'],
|
|
302
|
+
'path': item_path,
|
|
303
|
+
'url': item.get('url', ''),
|
|
304
|
+
'download_url': item.get('download_url', '')
|
|
305
|
+
})
|
|
306
|
+
|
|
307
|
+
elif item['type'] == 'dir':
|
|
308
|
+
# Excluir directorios comunes que no contienen componentes
|
|
309
|
+
if item['name'] not in ['node_modules', 'dist', 'build', '.git', 'test', 'tests', '__pycache__']:
|
|
310
|
+
recurse_directory(item_path)
|
|
311
|
+
|
|
312
|
+
recurse_directory(path)
|
|
313
|
+
return all_files
|
|
314
|
+
|
|
315
|
+
def search_code(self, query: str, per_page: int = 100) -> List[Dict]:
|
|
316
|
+
"""
|
|
317
|
+
Busca código en GitHub usando la API de búsqueda
|
|
318
|
+
|
|
319
|
+
Args:
|
|
320
|
+
query: Query de búsqueda (ej: '"bds_mobile" in:file filename:pubspec.yaml')
|
|
321
|
+
per_page: Número de resultados por página (máximo 100)
|
|
322
|
+
|
|
323
|
+
Returns:
|
|
324
|
+
Lista de diccionarios con información de archivos encontrados
|
|
325
|
+
"""
|
|
326
|
+
all_results = []
|
|
327
|
+
page = 1
|
|
328
|
+
|
|
329
|
+
while True:
|
|
330
|
+
url = "https://api.github.com/search/code"
|
|
331
|
+
params = {
|
|
332
|
+
'q': query,
|
|
333
|
+
'per_page': per_page,
|
|
334
|
+
'page': page
|
|
335
|
+
}
|
|
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)}")
|
|
359
|
+
break
|
|
360
|
+
|
|
361
|
+
return all_results
|
|
362
|
+
|
|
363
|
+
def search_projects_with_bds(self, platform: str, design_system_name: str = None) -> List[Dict]:
|
|
364
|
+
"""
|
|
365
|
+
Busca proyectos que usan el sistema de diseño BDS (mobile o web)
|
|
366
|
+
|
|
367
|
+
Args:
|
|
368
|
+
platform: 'mobile' o 'web'
|
|
369
|
+
design_system_name: Nombre del paquete del sistema de diseño
|
|
370
|
+
Si no se proporciona, usa valores por defecto:
|
|
371
|
+
- mobile: "bds_mobile"
|
|
372
|
+
- web: "@bancolombia/design-system-web"
|
|
373
|
+
|
|
374
|
+
Returns:
|
|
375
|
+
Lista de proyectos con información del repositorio incluyendo la versión
|
|
376
|
+
|
|
377
|
+
Raises:
|
|
378
|
+
ValueError: Si platform no es 'mobile' o 'web'
|
|
379
|
+
"""
|
|
380
|
+
# Validar plataforma
|
|
381
|
+
if platform not in ['mobile', 'web']:
|
|
382
|
+
raise ValueError(f"Platform debe ser 'mobile' o 'web', se recibió: {platform}")
|
|
383
|
+
|
|
384
|
+
# Configurar valores según la plataforma
|
|
385
|
+
if platform == 'mobile':
|
|
386
|
+
config_file = 'pubspec.yaml'
|
|
387
|
+
default_package_name = 'bds_mobile'
|
|
388
|
+
extract_version_method = CommonsReposClient.extract_package_version_from_pubspec
|
|
389
|
+
else: # web
|
|
390
|
+
config_file = 'package.json'
|
|
391
|
+
default_package_name = '@bancolombia/design-system-web'
|
|
392
|
+
extract_version_method = CommonsReposClient.extract_package_version_from_package_json
|
|
393
|
+
|
|
394
|
+
# Usar nombre de paquete por defecto si no se proporciona
|
|
395
|
+
package_name = design_system_name or default_package_name
|
|
396
|
+
|
|
397
|
+
# Agregar filtro de organización si self.owner está definido
|
|
398
|
+
org_filter = f" org:{self.owner}" if self.owner else ""
|
|
399
|
+
query = f'"{package_name}" in:file filename:{config_file}{org_filter}'
|
|
400
|
+
results = self.search_code(query)
|
|
401
|
+
|
|
402
|
+
projects = []
|
|
403
|
+
for item in results:
|
|
404
|
+
# Obtener contenido del archivo de configuración para extraer la versión
|
|
405
|
+
file_content = self._get_file_content_from_url(item['url'])
|
|
406
|
+
version = extract_version_method(file_content, package_name)
|
|
407
|
+
|
|
408
|
+
project_info = {
|
|
409
|
+
'name': item['repository']['name'],
|
|
410
|
+
'full_name': item['repository']['full_name'],
|
|
411
|
+
'owner': item['repository']['owner']['login'],
|
|
412
|
+
'repo_url': item['repository']['html_url'],
|
|
413
|
+
'file_path': item['path'],
|
|
414
|
+
'bds_version': version
|
|
415
|
+
}
|
|
416
|
+
projects.append(project_info)
|
|
417
|
+
|
|
418
|
+
return projects
|
|
419
|
+
|
|
420
|
+
def search_repositories(self, query: str, per_page: int = 100) -> List[Dict]:
|
|
421
|
+
"""
|
|
422
|
+
Busca repositorios en GitHub usando la API de búsqueda
|
|
423
|
+
|
|
424
|
+
Args:
|
|
425
|
+
query: Query de búsqueda (ej: 'NU0296001 mobile in:name org:grupobancolombia-innersource')
|
|
426
|
+
per_page: Número de resultados por página (máximo 100)
|
|
427
|
+
|
|
428
|
+
Returns:
|
|
429
|
+
Lista de diccionarios con información de repositorios encontrados
|
|
430
|
+
"""
|
|
431
|
+
all_results = []
|
|
432
|
+
page = 1
|
|
433
|
+
search_url = "https://api.github.com/search/repositories"
|
|
434
|
+
|
|
435
|
+
while True:
|
|
436
|
+
params = {
|
|
437
|
+
'q': query,
|
|
438
|
+
'per_page': per_page,
|
|
439
|
+
'page': page
|
|
440
|
+
}
|
|
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}")
|
|
466
|
+
break
|
|
467
|
+
|
|
468
|
+
return all_results
|
|
469
|
+
|
|
470
|
+
def _get_file_content_from_url(self, api_url: str) -> Optional[str]:
|
|
471
|
+
"""
|
|
472
|
+
Obtiene el contenido de un archivo desde una URL de la API de GitHub
|
|
473
|
+
|
|
474
|
+
Args:
|
|
475
|
+
api_url: URL de la API de GitHub para el archivo
|
|
476
|
+
|
|
477
|
+
Returns:
|
|
478
|
+
Contenido del archivo como string, o None si no existe
|
|
479
|
+
"""
|
|
480
|
+
response = self._request_with_backoff('get', api_url, headers=self.headers)
|
|
481
|
+
|
|
482
|
+
if response.status_code == 200:
|
|
483
|
+
content = response.json()
|
|
484
|
+
if content.get('encoding') == 'base64':
|
|
485
|
+
decoded_content = base64.b64decode(content['content']).decode('utf-8')
|
|
486
|
+
return decoded_content
|
|
487
|
+
return None
|
|
488
|
+
return None
|
|
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
|
|
|
2
2
|
|
|
3
3
|
setup(
|
|
4
4
|
name='commons_metrics',
|
|
5
|
-
version='0.0.
|
|
5
|
+
version='0.0.25',
|
|
6
6
|
description='A simple library for basic statistical calculations',
|
|
7
7
|
#long_description=open('USAGE.md').read(),
|
|
8
8
|
#long_description_content_type='text/markdown',
|
|
@@ -1,305 +0,0 @@
|
|
|
1
|
-
import requests
|
|
2
|
-
import base64
|
|
3
|
-
from typing import List, Dict, Optional
|
|
4
|
-
import os
|
|
5
|
-
from .commons_repos_client import CommonsReposClient
|
|
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 = CommonsReposClient.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 = CommonsReposClient.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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/update_design_components.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|