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.
Files changed (24) hide show
  1. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/PKG-INFO +1 -1
  2. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/__init__.py +1 -1
  3. commons_metrics-0.0.25/commons_metrics/github_api_client.py +488 -0
  4. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/PKG-INFO +1 -1
  5. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/setup.py +1 -1
  6. commons_metrics-0.0.23/commons_metrics/github_api_client.py +0 -305
  7. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/LICENSE +0 -0
  8. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/README.md +0 -0
  9. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/azure_devops_client.py +0 -0
  10. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/cache_manager.py +0 -0
  11. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/commons_repos_client.py +0 -0
  12. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/database.py +0 -0
  13. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/date_utils.py +0 -0
  14. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/repositories.py +0 -0
  15. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/s3_file_manager.py +0 -0
  16. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/text_simplifier.py +0 -0
  17. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/update_design_components.py +0 -0
  18. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/util.py +0 -0
  19. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics/variable_finder.py +0 -0
  20. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/SOURCES.txt +0 -0
  21. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/dependency_links.txt +0 -0
  22. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/requires.txt +0 -0
  23. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/commons_metrics.egg-info/top_level.txt +0 -0
  24. {commons_metrics-0.0.23 → commons_metrics-0.0.25}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commons_metrics
3
- Version: 0.0.23
3
+ Version: 0.0.25
4
4
  Summary: A simple library for basic statistical calculations
5
5
  Author: Bancolombia
6
6
  Author-email: omar.david.pino@email.com
@@ -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.23'
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
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: commons_metrics
3
- Version: 0.0.23
3
+ Version: 0.0.25
4
4
  Summary: A simple library for basic statistical calculations
5
5
  Author: Bancolombia
6
6
  Author-email: omar.david.pino@email.com
@@ -2,7 +2,7 @@ from setuptools import setup, find_packages
2
2
 
3
3
  setup(
4
4
  name='commons_metrics',
5
- version='0.0.23',
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