socialseed-e2e 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,51 @@
1
+ """
2
+ socialseed-e2e: Framework E2E para testing de APIs REST con Playwright.
3
+
4
+ Este paquete proporciona un framework completo para testing end-to-end
5
+ de APIs REST, extraído y generalizado desde el proyecto SocialSeed.
6
+
7
+ Características principales:
8
+ - Arquitectura hexagonal (core agnóstico)
9
+ - Configuración centralizada via YAML
10
+ - Carga dinámica de módulos de test
11
+ - Soporte para API Gateway o conexiones directas
12
+ - CLI completo con Rich y Click
13
+
14
+ Uso básico:
15
+ >>> from socialseed_e2e import BasePage, ApiConfigLoader
16
+ >>> config = ApiConfigLoader.load()
17
+ >>> page = BasePage(config.base_url)
18
+ >>> response = page.get("/endpoint")
19
+
20
+ Para más información, visita: https://github.com/daironpf/socialseed-e2e
21
+ """
22
+
23
+ __version__ = "0.1.0"
24
+ __version_info__ = (0, 1, 0)
25
+ __author__ = "Dairon Pérez Frías"
26
+ __email__ = "dairon.perezfrias@gmail.com"
27
+ __license__ = "MIT"
28
+ __copyright__ = "Copyright 2026 Dairon Pérez Frías"
29
+ __url__ = "https://github.com/daironpf/socialseed-e2e"
30
+
31
+ from socialseed_e2e.cli import main
32
+
33
+ # Hacer disponibles las clases principales
34
+ from socialseed_e2e.core.base_page import BasePage
35
+ from socialseed_e2e.core.config_loader import ApiConfigLoader, get_config, get_service_config
36
+ from socialseed_e2e.core.loaders import ModuleLoader
37
+ from socialseed_e2e.core.models import ServiceConfig, TestContext
38
+ from socialseed_e2e.core.test_orchestrator import TestOrchestrator
39
+
40
+ __all__ = [
41
+ "BasePage",
42
+ "ApiConfigLoader",
43
+ "get_config",
44
+ "get_service_config",
45
+ "ModuleLoader",
46
+ "TestOrchestrator",
47
+ "ServiceConfig",
48
+ "TestContext",
49
+ "main",
50
+ "__version__",
51
+ ]
@@ -0,0 +1,21 @@
1
+ """
2
+ Version information for socialseed-e2e.
3
+
4
+ This module contains version information and metadata about the package.
5
+ """
6
+
7
+ __version__ = "0.1.0"
8
+ __version_info__ = (0, 1, 0)
9
+ __author__ = "Dairon Pérez Frías"
10
+ __email__ = "dairon.perezfrias@gmail.com"
11
+ __license__ = "MIT"
12
+ __copyright__ = "Copyright 2026 Dairon Pérez Frías"
13
+ __url__ = "https://github.com/daironpf/socialseed-e2e"
14
+ __description__ = "Framework E2E para testing de APIs REST con Playwright"
15
+ __keywords__ = ["testing", "e2e", "api", "playwright", "rest", "framework"]
16
+
17
+ # Minimum supported Python version
18
+ __python_requires__ = ">=3.9"
19
+
20
+ # Playwright version compatibility
21
+ __playwright_version__ = ">=1.40.0"
socialseed_e2e/cli.py ADDED
@@ -0,0 +1,611 @@
1
+ #!/usr/bin/env python3
2
+ """CLI module for socialseed-e2e framework.
3
+
4
+ This module provides the command-line interface for the E2E testing framework,
5
+ enabling developers and AI agents to create, manage, and run API tests.
6
+ """
7
+
8
+ import os
9
+ import subprocess
10
+ import sys
11
+ from pathlib import Path
12
+ from typing import Optional
13
+
14
+ import click
15
+ from rich.console import Console
16
+ from rich.panel import Panel
17
+ from rich.table import Table
18
+ from rich.text import Text
19
+
20
+ from socialseed_e2e import __version__
21
+ from socialseed_e2e.core.config_loader import ApiConfigLoader, ConfigError
22
+ from socialseed_e2e.utils import TemplateEngine, to_class_name, to_snake_case
23
+
24
+ console = Console()
25
+
26
+
27
+ @click.group()
28
+ @click.version_option(version=__version__, prog_name="socialseed-e2e")
29
+ def cli():
30
+ """socialseed-e2e: Framework E2E para APIs REST.
31
+
32
+ Un framework agnóstico de servicios para testing End-to-End de APIs REST,
33
+ diseñado para desarrolladores y agentes de IA.
34
+ """
35
+ pass
36
+
37
+
38
+ @cli.command()
39
+ @click.argument("directory", default=".", required=False)
40
+ @click.option("--force", is_flag=True, help="Sobrescribir archivos existentes")
41
+ def init(directory: str, force: bool):
42
+ """Inicializa un nuevo proyecto E2E.
43
+
44
+ Crea la estructura de directorios y archivos de configuración inicial.
45
+
46
+ Args:
47
+ directory: Directorio donde crear el proyecto (default: directorio actual)
48
+ force: Si es True, sobrescribe archivos existentes
49
+ """
50
+ target_path = Path(directory).resolve()
51
+
52
+ console.print(f"\n🌱 [bold green]Inicializando proyecto E2E en:[/bold green] {target_path}\n")
53
+
54
+ # Crear estructura de directorios
55
+ dirs_to_create = [
56
+ target_path / "services",
57
+ target_path / "tests",
58
+ target_path / ".github" / "workflows",
59
+ ]
60
+
61
+ created_dirs = []
62
+ for dir_path in dirs_to_create:
63
+ if not dir_path.exists():
64
+ dir_path.mkdir(parents=True)
65
+ created_dirs.append(
66
+ dir_path.name
67
+ if dir_path.parent == target_path
68
+ else str(dir_path.relative_to(target_path))
69
+ )
70
+ console.print(f" [green]✓[/green] Creado: {dir_path.relative_to(target_path)}")
71
+ else:
72
+ console.print(f" [yellow]⚠[/yellow] Ya existe: {dir_path.relative_to(target_path)}")
73
+
74
+ # Crear archivo de configuración
75
+ config_path = target_path / "e2e.conf"
76
+ if not config_path.exists() or force:
77
+ engine = TemplateEngine()
78
+ engine.render_to_file(
79
+ "e2e.conf.template",
80
+ {
81
+ "environment": "dev",
82
+ "timeout": "30000",
83
+ "user_agent": "socialseed-e2e/1.0",
84
+ "verbose": "true",
85
+ "services_config": "",
86
+ },
87
+ str(config_path),
88
+ overwrite=force,
89
+ )
90
+ console.print(f" [green]✓[/green] Creado: e2e.conf")
91
+ else:
92
+ console.print(f" [yellow]⚠[/yellow] Ya existe: e2e.conf (usa --force para sobrescribir)")
93
+
94
+ # Crear .gitignore
95
+ gitignore_path = target_path / ".gitignore"
96
+ if not gitignore_path.exists() or force:
97
+ gitignore_content = """# Python
98
+ __pycache__/
99
+ *.py[cod]
100
+ *$py.class
101
+ *.so
102
+ .Python
103
+ *.egg-info/
104
+ dist/
105
+ build/
106
+
107
+ # Virtual environments
108
+ venv/
109
+ env/
110
+ ENV/
111
+
112
+ # IDE
113
+ .vscode/
114
+ .idea/
115
+ *.swp
116
+ *.swo
117
+
118
+ # E2E Framework
119
+ test-results/
120
+ .coverage
121
+ htmlcov/
122
+ """
123
+ gitignore_path.write_text(gitignore_content)
124
+ console.print(f" [green]✓[/green] Creado: .gitignore")
125
+ else:
126
+ console.print(f" [yellow]⚠[/yellow] Ya existe: .gitignore")
127
+
128
+ # Mostrar mensaje de éxito
129
+ console.print(f"\n[bold green]✅ Proyecto inicializado correctamente![/bold green]\n")
130
+
131
+ console.print(
132
+ Panel(
133
+ "[bold]Próximos pasos:[/bold]\n\n"
134
+ "1. Edita [cyan]e2e.conf[/cyan] para configurar tu API\n"
135
+ "2. Ejecuta: [cyan]e2e new-service <nombre>[/cyan]\n"
136
+ "3. Ejecuta: [cyan]e2e new-test <nombre> --service <svc>[/cyan]\n"
137
+ "4. Ejecuta: [cyan]e2e run[/cyan] para correr tests",
138
+ title="🚀 Empezar",
139
+ border_style="green",
140
+ )
141
+ )
142
+
143
+
144
+ @cli.command()
145
+ @click.argument("name")
146
+ @click.option("--base-url", default="http://localhost:8080", help="URL base del servicio")
147
+ @click.option("--health-endpoint", default="/health", help="Endpoint de health check")
148
+ def new_service(name: str, base_url: str, health_endpoint: str):
149
+ """Crea un nuevo servicio con scaffolding.
150
+
151
+ Args:
152
+ name: Nombre del servicio (ej: users-api)
153
+ base_url: URL base del servicio
154
+ health_endpoint: Endpoint para health checks
155
+ """
156
+ console.print(f"\n🔧 [bold blue]Creando servicio:[/bold blue] {name}\n")
157
+
158
+ # Verificar que estamos en un proyecto E2E
159
+ if not _is_e2e_project():
160
+ console.print("[red]❌ Error:[/red] No se encontró e2e.conf. ¿Estás en un proyecto E2E?")
161
+ console.print(" Ejecuta: [cyan]e2e init[/cyan] primero")
162
+ sys.exit(1)
163
+
164
+ # Crear estructura del servicio
165
+ service_path = Path("services") / name
166
+ modules_path = service_path / "modules"
167
+
168
+ try:
169
+ service_path.mkdir(parents=True)
170
+ modules_path.mkdir()
171
+ console.print(f" [green]✓[/green] Creado: services/{name}/")
172
+ console.print(f" [green]✓[/green] Creado: services/{name}/modules/")
173
+ except FileExistsError:
174
+ console.print(f" [yellow]⚠[/yellow] El servicio '{name}' ya existe")
175
+ if not click.confirm("¿Deseas continuar y sobrescribir archivos?"):
176
+ return
177
+
178
+ # Crear __init__.py
179
+ _create_file(service_path / "__init__.py", f'"""Servicio {name}."""\n')
180
+ _create_file(modules_path / "__init__.py", f'"""Módulos de test para {name}."""\n')
181
+ console.print(f" [green]✓[/green] Creado: services/{name}/__init__.py")
182
+ console.print(f" [green]✓[/green] Creado: services/{name}/modules/__init__.py")
183
+
184
+ # Inicializar TemplateEngine
185
+ engine = TemplateEngine()
186
+
187
+ # Variables para los templates
188
+ class_name = _to_class_name(name)
189
+ snake_case_name = to_snake_case(name)
190
+ template_vars = {
191
+ "service_name": name,
192
+ "class_name": class_name,
193
+ "snake_case_name": snake_case_name,
194
+ "endpoint_prefix": "entities",
195
+ }
196
+
197
+ # Crear página del servicio
198
+ engine.render_to_file(
199
+ "service_page.py.template",
200
+ template_vars,
201
+ str(service_path / f"{snake_case_name}_page.py"),
202
+ overwrite=False,
203
+ )
204
+ console.print(f" [green]✓[/green] Creado: services/{name}/{snake_case_name}_page.py")
205
+
206
+ # Crear archivo de configuración
207
+ engine.render_to_file(
208
+ "config.py.template", template_vars, str(service_path / "config.py"), overwrite=False
209
+ )
210
+ console.print(f" [green]✓[/green] Creado: services/{name}/config.py")
211
+
212
+ # Crear data_schema.py
213
+ engine.render_to_file(
214
+ "data_schema.py.template",
215
+ template_vars,
216
+ str(service_path / "data_schema.py"),
217
+ overwrite=False,
218
+ )
219
+ console.print(f" [green]✓[/green] Creado: services/{name}/data_schema.py")
220
+
221
+ # Actualizar e2e.conf
222
+ _update_e2e_conf(name, base_url, health_endpoint)
223
+
224
+ console.print(f"\n[bold green]✅ Servicio '{name}' creado correctamente![/bold green]\n")
225
+
226
+ console.print(
227
+ Panel(
228
+ f"[bold]Próximos pasos:[/bold]\n\n"
229
+ f"1. Edita [cyan]services/{name}/data_schema.py[/cyan] para definir tus DTOs\n"
230
+ f"2. Ejecuta: [cyan]e2e new-test <nombre> --service {name}[/cyan]\n"
231
+ f"3. Ejecuta: [cyan]e2e run --service {name}[/cyan]",
232
+ title="🚀 Continuar",
233
+ border_style="blue",
234
+ )
235
+ )
236
+
237
+
238
+ @cli.command()
239
+ @click.argument("name")
240
+ @click.option("--service", "-s", required=True, help="Nombre del servicio")
241
+ @click.option("--description", "-d", default="", help="Descripción del test")
242
+ def new_test(name: str, service: str, description: str):
243
+ """Crea un nuevo módulo de test.
244
+
245
+ Args:
246
+ name: Nombre del test (ej: login, create-user)
247
+ service: Servicio al que pertenece el test
248
+ description: Descripción opcional del test
249
+ """
250
+ console.print(f"\n📝 [bold cyan]Creando test:[/bold cyan] {name}\n")
251
+
252
+ # Verificar que estamos en un proyecto E2E
253
+ if not _is_e2e_project():
254
+ console.print("[red]❌ Error:[/red] No se encontró e2e.conf. ¿Estás en un proyecto E2E?")
255
+ sys.exit(1)
256
+
257
+ # Verificar que el servicio existe
258
+ service_path = Path("services") / service
259
+ modules_path = service_path / "modules"
260
+
261
+ if not service_path.exists():
262
+ console.print(f"[red]❌ Error:[/red] El servicio '{service}' no existe.")
263
+ console.print(f" Crea el servicio primero: [cyan]e2e new-service {service}[/cyan]")
264
+ sys.exit(1)
265
+
266
+ if not modules_path.exists():
267
+ modules_path.mkdir(parents=True)
268
+
269
+ # Encontrar siguiente número disponible
270
+ existing_tests = sorted(modules_path.glob("[0-9][0-9]_*.py"))
271
+ if existing_tests:
272
+ last_num = int(existing_tests[-1].name[:2])
273
+ next_num = last_num + 1
274
+ else:
275
+ next_num = 1
276
+
277
+ test_filename = f"{next_num:02d}_{name}_flow.py"
278
+ test_path = modules_path / test_filename
279
+
280
+ # Verificar si ya existe
281
+ if test_path.exists():
282
+ console.print(f"[yellow]⚠[/yellow] El test '{name}' ya existe.")
283
+ if not click.confirm("¿Deseas sobrescribirlo?"):
284
+ return
285
+
286
+ # Inicializar TemplateEngine
287
+ engine = TemplateEngine()
288
+
289
+ # Variables para el template
290
+ class_name = _to_class_name(service)
291
+ snake_case_name = to_snake_case(service)
292
+ test_description = description or f"Test flow for {name}"
293
+
294
+ template_vars = {
295
+ "service_name": service,
296
+ "class_name": class_name,
297
+ "snake_case_name": snake_case_name,
298
+ "test_name": name,
299
+ "test_description": test_description,
300
+ }
301
+
302
+ # Crear test usando template
303
+ engine.render_to_file("test_module.py.template", template_vars, str(test_path), overwrite=False)
304
+ console.print(f" [green]✓[/green] Creado: services/{service}/modules/{test_filename}")
305
+
306
+ console.print(f"\n[bold green]✅ Test '{name}' creado correctamente![/bold green]\n")
307
+
308
+ console.print(
309
+ Panel(
310
+ f"[bold]Próximos pasos:[/bold]\n\n"
311
+ f"1. Edita [cyan]services/{service}/modules/{test_filename}[/cyan]\n"
312
+ f"2. Implementa la lógica del test\n"
313
+ f"3. Ejecuta: [cyan]e2e run --service {service}[/cyan]",
314
+ title="🚀 Implementar",
315
+ border_style="cyan",
316
+ )
317
+ )
318
+
319
+
320
+ @cli.command()
321
+ @click.option("--service", "-s", help="Filtrar por servicio específico")
322
+ @click.option("--module", "-m", help="Filtrar por módulo específico")
323
+ @click.option("--verbose", "-v", is_flag=True, help="Modo verbose")
324
+ @click.option(
325
+ "--output", "-o", type=click.Choice(["text", "json"]), default="text", help="Formato de salida"
326
+ )
327
+ def run(service: Optional[str], module: Optional[str], verbose: bool, output: str):
328
+ """Ejecuta los tests E2E.
329
+
330
+ Descubre y ejecuta automáticamente todos los tests disponibles.
331
+
332
+ Args:
333
+ service: Si se especifica, solo ejecuta tests de este servicio
334
+ module: Si se especifica, solo ejecuta este módulo de test
335
+ verbose: Si es True, muestra información detallada
336
+ output: Formato de salida (text o json)
337
+ """
338
+ from .core.test_orchestrator import TestOrchestrator
339
+
340
+ console.print(f"\n🚀 [bold green]socialseed-e2e v{__version__}[/bold green]")
341
+ console.print("═" * 50)
342
+ console.print()
343
+
344
+ # Verificar configuración
345
+ try:
346
+ loader = ApiConfigLoader()
347
+ config = loader.load()
348
+ console.print(f"📋 [cyan]Configuración:[/cyan] {loader._config_path}")
349
+ console.print(f"🌍 [cyan]Environment:[/cyan] {config.environment}")
350
+ console.print()
351
+ except ConfigError as e:
352
+ console.print(f"[red]❌ Error de configuración:[/red] {e}")
353
+ console.print(" Ejecuta: [cyan]e2e init[/cyan] para crear un proyecto")
354
+ sys.exit(1)
355
+
356
+ # TODO: Implementar ejecución real de tests
357
+ # Por ahora, mostramos información de descubrimiento
358
+
359
+ if service:
360
+ console.print(f"🔍 [yellow]Filtrando por servicio:[/yellow] {service}")
361
+ if module:
362
+ console.print(f"🔍 [yellow]Filtrando por módulo:[/yellow] {module}")
363
+ if verbose:
364
+ console.print(f"📢 [yellow]Modo verbose activado[/yellow]")
365
+
366
+ console.print()
367
+ console.print("[yellow]⚠ Nota:[/yellow] La ejecución de tests aún no está implementada")
368
+ console.print(" Este es un placeholder para la versión 0.1.0")
369
+ console.print()
370
+
371
+ # Mostrar tabla de servicios encontrados
372
+ services_path = Path("services")
373
+ if services_path.exists():
374
+ services = [
375
+ d.name for d in services_path.iterdir() if d.is_dir() and not d.name.startswith("__")
376
+ ]
377
+
378
+ if services:
379
+ table = Table(title="Servicios Encontrados")
380
+ table.add_column("Servicio", style="cyan")
381
+ table.add_column("Tests", style="green")
382
+ table.add_column("Estado", style="yellow")
383
+
384
+ for svc in services:
385
+ modules_path = services_path / svc / "modules"
386
+ if modules_path.exists():
387
+ test_count = len(list(modules_path.glob("[0-9][0-9]_*.py")))
388
+ table.add_row(svc, str(test_count), "Ready" if test_count > 0 else "Empty")
389
+ else:
390
+ table.add_row(svc, "0", "No modules")
391
+
392
+ console.print(table)
393
+ else:
394
+ console.print("[yellow]⚠ No se encontraron servicios[/yellow]")
395
+ console.print(" Crea uno con: [cyan]e2e new-service <nombre>[/cyan]")
396
+ else:
397
+ console.print("[red]❌ No se encontró el directorio 'services/'[/red]")
398
+
399
+ console.print()
400
+ console.print("═" * 50)
401
+ console.print("[bold]Para implementar la ejecución real, contribuye en:[/bold]")
402
+ console.print("[cyan]https://github.com/daironpf/socialseed-e2e[/cyan]")
403
+
404
+
405
+ @cli.command()
406
+ def doctor():
407
+ """Verifica la instalación y dependencias.
408
+
409
+ Comprueba que todo esté correctamente configurado para usar el framework.
410
+ """
411
+ console.print("\n🏥 [bold green]socialseed-e2e Doctor[/bold green]\n")
412
+
413
+ checks = []
414
+
415
+ # Verificar Python
416
+ python_version = f"{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
417
+ checks.append(("Python", python_version, sys.version_info >= (3, 9)))
418
+
419
+ # Verificar Playwright
420
+ try:
421
+ from importlib.metadata import version
422
+
423
+ pw_version = version("playwright")
424
+ checks.append(("Playwright", pw_version, True))
425
+ except Exception:
426
+ checks.append(("Playwright", "No instalado", False))
427
+
428
+ # Verificar browsers de Playwright
429
+ try:
430
+ result = subprocess.run(
431
+ ["playwright", "install", "--help"], capture_output=True, text=True, timeout=5
432
+ )
433
+ browsers_installed = result.returncode == 0
434
+ checks.append(("Playwright CLI", "Disponible", browsers_installed))
435
+ except (subprocess.TimeoutExpired, FileNotFoundError):
436
+ checks.append(("Playwright CLI", "No disponible", False))
437
+
438
+ # Verificar Pydantic
439
+ try:
440
+ import pydantic
441
+
442
+ checks.append(("Pydantic", pydantic.__version__, True))
443
+ except ImportError:
444
+ checks.append(("Pydantic", "No instalado", False))
445
+
446
+ # Verificar e2e.conf
447
+ if _is_e2e_project():
448
+ checks.append(("Configuración", "e2e.conf encontrado", True))
449
+ else:
450
+ checks.append(("Configuración", "e2e.conf no encontrado", False))
451
+
452
+ # Verificar estructura de directorios
453
+ services_exists = Path("services").exists()
454
+ tests_exists = Path("tests").exists()
455
+ checks.append(
456
+ ("Directorio services/", "OK" if services_exists else "No encontrado", services_exists)
457
+ )
458
+ checks.append(("Directorio tests/", "OK" if tests_exists else "No encontrado", tests_exists))
459
+
460
+ # Mostrar resultados
461
+ table = Table(title="Verificación del Sistema")
462
+ table.add_column("Componente", style="cyan")
463
+ table.add_column("Versión/Estado", style="white")
464
+ table.add_column("Estado", style="bold")
465
+
466
+ all_ok = True
467
+ for name, value, ok in checks:
468
+ status = "[green]✓[/green]" if ok else "[red]✗[/red]"
469
+ table.add_row(name, value, status)
470
+ if not ok:
471
+ all_ok = False
472
+
473
+ console.print(table)
474
+
475
+ console.print()
476
+ if all_ok:
477
+ console.print("[bold green]✅ Todo está configurado correctamente![/bold green]")
478
+ else:
479
+ console.print("[bold yellow]⚠ Se encontraron algunos problemas[/bold yellow]")
480
+ console.print()
481
+ console.print("[cyan]Soluciones sugeridas:[/cyan]")
482
+
483
+ if not any(name == "Playwright" and ok for name, _, ok in checks):
484
+ console.print(" • Instala Playwright: [white]pip install playwright[/white]")
485
+ if not any(name == "Playwright CLI" and ok for name, _, ok in checks):
486
+ console.print(" • Instala browsers: [white]playwright install chromium[/white]")
487
+ if not any(name == "Pydantic" and ok for name, _, ok in checks):
488
+ console.print(" • Instala dependencias: [white]pip install socialseed-e2e[/white]")
489
+ if not _is_e2e_project():
490
+ console.print(" • Inicializa proyecto: [white]e2e init[/white]")
491
+
492
+ console.print()
493
+
494
+
495
+ @cli.command()
496
+ def config():
497
+ """Muestra y valida la configuración actual.
498
+
499
+ Muestra la configuración cargada desde e2e.conf y valida su sintaxis.
500
+ """
501
+ console.print("\n⚙️ [bold blue]Configuración E2E[/bold blue]\n")
502
+
503
+ try:
504
+ loader = ApiConfigLoader()
505
+ config = loader.load()
506
+
507
+ console.print(f"📋 [cyan]Configuración:[/cyan] {loader._config_path}")
508
+ console.print(f"🌍 [cyan]Environment:[/cyan] {config.environment}")
509
+ console.print(f"[cyan]Timeout:[/cyan] {config.timeout}ms")
510
+ console.print(f"[cyan]Verbose:[/cyan] {config.verbose}")
511
+ console.print()
512
+
513
+ if config.services:
514
+ table = Table(title="Servicios Configurados")
515
+ table.add_column("Nombre", style="cyan")
516
+ table.add_column("Base URL", style="green")
517
+ table.add_column("Health", style="yellow")
518
+ table.add_column("Requerido", style="white")
519
+
520
+ for name, svc in config.services.items():
521
+ table.add_row(
522
+ name, svc.base_url, svc.health_endpoint or "N/A", "✓" if svc.required else "✗"
523
+ )
524
+
525
+ console.print(table)
526
+ else:
527
+ console.print("[yellow]⚠ No hay servicios configurados[/yellow]")
528
+ console.print(" Usa: [cyan]e2e new-service <nombre>[/cyan]")
529
+
530
+ console.print()
531
+ console.print("[bold green]✅ Configuración válida[/bold green]")
532
+
533
+ except ConfigError as e:
534
+ console.print(f"[red]❌ Error de configuración:[/red] {e}")
535
+ sys.exit(1)
536
+ except Exception as e:
537
+ console.print(f"[red]❌ Error inesperado:[/red] {e}")
538
+ sys.exit(1)
539
+
540
+
541
+ # Funciones auxiliares
542
+
543
+
544
+ def _is_e2e_project() -> bool:
545
+ """Verifica si el directorio actual es un proyecto E2E."""
546
+ return Path("e2e.conf").exists()
547
+
548
+
549
+ def _to_class_name(name: str) -> str:
550
+ """Convierte un nombre de servicio a nombre de clase.
551
+
552
+ Args:
553
+ name: Nombre del servicio (ej: users-api)
554
+
555
+ Returns:
556
+ str: Nombre de clase (ej: UsersApi)
557
+ """
558
+ return to_class_name(name)
559
+
560
+
561
+ def _create_file(path: Path, content: str) -> None:
562
+ """Crea un archivo con el contenido especificado.
563
+
564
+ Args:
565
+ path: Ruta del archivo
566
+ content: Contenido a escribir
567
+ """
568
+ path.write_text(content)
569
+
570
+
571
+ def _update_e2e_conf(service_name: str, base_url: str, health_endpoint: str) -> None:
572
+ """Actualiza e2e.conf para incluir el nuevo servicio.
573
+
574
+ Args:
575
+ service_name: Nombre del servicio
576
+ base_url: URL base
577
+ health_endpoint: Endpoint de health check
578
+ """
579
+ config_path = Path("e2e.conf")
580
+
581
+ if not config_path.exists():
582
+ return
583
+
584
+ content = config_path.read_text()
585
+
586
+ # Verificar si ya existe la sección de servicios
587
+ if "services:" not in content:
588
+ content += "\nservices:\n"
589
+
590
+ # Agregar configuración del servicio
591
+ service_config = f""" {service_name}:
592
+ name: {service_name}-service
593
+ base_url: {base_url}
594
+ health_endpoint: {health_endpoint}
595
+ timeout: 5000
596
+ auto_start: false
597
+ required: true
598
+ """
599
+
600
+ content += service_config
601
+ config_path.write_text(content)
602
+ console.print(f" [green]✓[/green] Actualizado: e2e.conf")
603
+
604
+
605
+ def main():
606
+ """Entry point for the CLI."""
607
+ cli()
608
+
609
+
610
+ if __name__ == "__main__":
611
+ main()