vega-framework 0.1.18__tar.gz → 0.1.20__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 (75) hide show
  1. {vega_framework-0.1.18 → vega_framework-0.1.20}/PKG-INFO +1 -1
  2. {vega_framework-0.1.18 → vega_framework-0.1.20}/pyproject.toml +1 -1
  3. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/commands/generate.py +10 -11
  4. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/commands/init.py +9 -1
  5. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/scaffolds/fastapi.py +2 -2
  6. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/__init__.py +4 -0
  7. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/cli/command.py.j2 +4 -4
  8. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/cli/command_simple.py.j2 +4 -4
  9. vega_framework-0.1.20/vega/cli/templates/cli/commands_init.py.j2 +12 -0
  10. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/components.py +10 -0
  11. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/main.py.j2 +16 -2
  12. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/main_fastapi.py.j2 +8 -1
  13. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/main_standard.py.j2 +8 -1
  14. vega_framework-0.1.20/vega/cli/templates/web/routes_init_autodiscovery.py.j2 +12 -0
  15. vega_framework-0.1.20/vega/discovery/__init__.py +5 -0
  16. vega_framework-0.1.20/vega/discovery/commands.py +104 -0
  17. vega_framework-0.1.20/vega/discovery/routes.py +129 -0
  18. {vega_framework-0.1.18 → vega_framework-0.1.20}/LICENSE +0 -0
  19. {vega_framework-0.1.18 → vega_framework-0.1.20}/README.md +0 -0
  20. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/__init__.py +0 -0
  21. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/__init__.py +0 -0
  22. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/commands/__init__.py +0 -0
  23. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/commands/add.py +0 -0
  24. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/commands/migrate.py +0 -0
  25. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/commands/update.py +0 -0
  26. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/main.py +0 -0
  27. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/scaffolds/__init__.py +0 -0
  28. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/scaffolds/sqlalchemy.py +0 -0
  29. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/domain/entity.py.j2 +0 -0
  30. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/domain/interactor.py.j2 +0 -0
  31. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/domain/mediator.py.j2 +0 -0
  32. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/domain/repository_interface.py.j2 +0 -0
  33. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/domain/service_interface.py.j2 +0 -0
  34. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/infrastructure/model.py.j2 +0 -0
  35. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/infrastructure/repository_impl.py.j2 +0 -0
  36. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/infrastructure/service_impl.py.j2 +0 -0
  37. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/loader.py +0 -0
  38. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/.env.example +0 -0
  39. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/.gitignore +0 -0
  40. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/ARCHITECTURE.md.j2 +0 -0
  41. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/README.md.j2 +0 -0
  42. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/config.py.j2 +0 -0
  43. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/pyproject.toml.j2 +0 -0
  44. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/project/settings.py.j2 +0 -0
  45. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/sqlalchemy/alembic.ini.j2 +0 -0
  46. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/sqlalchemy/database_manager.py.j2 +0 -0
  47. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/sqlalchemy/env.py.j2 +0 -0
  48. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/sqlalchemy/script.py.mako +0 -0
  49. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/__init__.py.j2 +0 -0
  50. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/app.py.j2 +0 -0
  51. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/health_route.py.j2 +0 -0
  52. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/main.py.j2 +0 -0
  53. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/middleware.py.j2 +0 -0
  54. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/models_init.py.j2 +0 -0
  55. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/router.py.j2 +0 -0
  56. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/routes_init.py.j2 +0 -0
  57. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/user_models.py.j2 +0 -0
  58. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/templates/web/users_route.py.j2 +0 -0
  59. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/utils/__init__.py +0 -0
  60. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/utils/async_support.py +0 -0
  61. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/utils/messages.py +0 -0
  62. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/utils/naming.py +0 -0
  63. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/cli/utils/validators.py +0 -0
  64. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/di/__init__.py +0 -0
  65. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/di/container.py +0 -0
  66. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/di/decorators.py +0 -0
  67. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/di/errors.py +0 -0
  68. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/di/scope.py +0 -0
  69. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/patterns/__init__.py +0 -0
  70. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/patterns/interactor.py +0 -0
  71. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/patterns/mediator.py +0 -0
  72. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/patterns/repository.py +0 -0
  73. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/patterns/service.py +0 -0
  74. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/settings/__init__.py +0 -0
  75. {vega_framework-0.1.18 → vega_framework-0.1.20}/vega/settings/base.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: vega-framework
3
- Version: 0.1.18
3
+ Version: 0.1.20
4
4
  Summary: Enterprise-ready Python framework that enforces Clean Architecture for building maintainable and scalable applications.
5
5
  License: MIT
6
6
  License-File: LICENSE
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "vega-framework"
3
- version = "0.1.18"
3
+ version = "0.1.20"
4
4
  description = "Enterprise-ready Python framework that enforces Clean Architecture for building maintainable and scalable applications."
5
5
  authors = ["Roberto Ferro"]
6
6
  license = "MIT"
@@ -424,10 +424,11 @@ def _generate_router(project_root: Path, project_name: str, name: str) -> None:
424
424
  routes_path = web_path / "routes"
425
425
  routes_path.mkdir(exist_ok=True)
426
426
 
427
- # Check if __init__.py exists
427
+ # Check if __init__.py exists, create with auto-discovery if not
428
428
  init_file = routes_path / "__init__.py"
429
429
  if not init_file.exists():
430
- init_file.write_text('"""API Routes"""\n')
430
+ from vega.cli.templates import render_fastapi_routes_init_autodiscovery
431
+ init_file.write_text(render_fastapi_routes_init_autodiscovery())
431
432
  click.echo(f"+ Created {click.style(str(init_file.relative_to(project_root)), fg='green')}")
432
433
 
433
434
  # Generate router file
@@ -442,14 +443,12 @@ def _generate_router(project_root: Path, project_name: str, name: str) -> None:
442
443
 
443
444
  click.echo(f"+ Created {click.style(str(router_file.relative_to(project_root)), fg='green')}")
444
445
 
445
- # Register the router in routes/__init__.py
446
- _register_router_in_init(project_root, resource_file, resource_name)
447
-
448
446
  # Instructions for next steps
449
447
  click.echo(f"\nNext steps:")
450
448
  click.echo(f" 1. Create Pydantic models in presentation/web/models/{resource_file}_models.py")
451
449
  click.echo(f" 2. Implement domain interactors for {resource_name} operations")
452
450
  click.echo(f" 3. Replace in-memory storage with actual use cases")
451
+ click.echo(click.style(f" (Router auto-discovered from web/routes/)", fg='dim'))
453
452
 
454
453
 
455
454
  def _generate_middleware(project_root: Path, project_name: str, class_name: str, file_name: str) -> None:
@@ -688,11 +687,12 @@ def _generate_command(project_root: Path, project_name: str, name: str, is_async
688
687
  # Create commands directory if it doesn't exist
689
688
  commands_path = cli_path / "commands"
690
689
  commands_path.mkdir(exist_ok=True)
691
-
692
- # Check if __init__.py exists
690
+
691
+ # Check if __init__.py exists, create with auto-discovery if not
693
692
  init_file = commands_path / "__init__.py"
694
693
  if not init_file.exists():
695
- init_file.write_text('"""CLI Commands"""\n')
694
+ from vega.cli.templates import render_cli_commands_init
695
+ init_file.write_text(render_cli_commands_init())
696
696
  click.echo(f"+ Created {click.style(str(init_file.relative_to(project_root)), fg='green')}")
697
697
 
698
698
  # Convert name to snake_case for command and file
@@ -803,8 +803,7 @@ def _generate_command(project_root: Path, project_name: str, name: str, is_async
803
803
  # Instructions for next steps
804
804
  click.echo(f"\nNext steps:")
805
805
  click.echo(f" 1. Implement your command logic in {command_file.relative_to(project_root)}")
806
- click.echo(f" 2. Import and register in main.py:")
807
- click.echo(click.style(f" from presentation.cli.commands.{file_name} import {file_name}", fg='cyan'))
808
- click.echo(click.style(f" cli.add_command({file_name})", fg='cyan'))
806
+ click.echo(f" 2. Run your command: python main.py {command_name}")
807
+ click.echo(click.style(f" (Commands are auto-discovered from cli/commands/)", fg='dim'))
809
808
  if with_interactor:
810
809
  click.echo(f" 3. Create interactor: vega generate interactor {interactor_name}")
@@ -47,7 +47,15 @@ def init_project(project_name: str, template: str, parent_path: str):
47
47
  for directory in directories:
48
48
  dir_path = project_path / directory
49
49
  dir_path.mkdir(parents=True, exist_ok=True)
50
- (dir_path / "__init__.py").write_text("")
50
+
51
+ # Use auto-discovery template for cli/commands
52
+ if "cli" in directory and "commands" in directory:
53
+ from vega.cli.templates import render_cli_commands_init
54
+ content = render_cli_commands_init()
55
+ (dir_path / "__init__.py").write_text(content)
56
+ else:
57
+ (dir_path / "__init__.py").write_text("")
58
+
51
59
  click.echo(f" + Created {directory}/")
52
60
 
53
61
  # Create __init__.py files
@@ -9,7 +9,7 @@ from vega.cli.templates import (
9
9
  render_fastapi_app,
10
10
  render_fastapi_health_route,
11
11
  render_fastapi_main,
12
- render_fastapi_routes_init,
12
+ render_fastapi_routes_init_autodiscovery,
13
13
  render_fastapi_user_route,
14
14
  render_pydantic_models_init,
15
15
  render_pydantic_user_models,
@@ -37,7 +37,7 @@ def create_fastapi_scaffold(
37
37
  (web_dir / "__init__.py", render_web_package_init()),
38
38
  (web_dir / "app.py", render_fastapi_app(project_name)),
39
39
  (web_dir / "main.py", render_fastapi_main(project_name)),
40
- (routes_dir / "__init__.py", render_fastapi_routes_init()),
40
+ (routes_dir / "__init__.py", render_fastapi_routes_init_autodiscovery()),
41
41
  (routes_dir / "health.py", render_fastapi_health_route()),
42
42
  (routes_dir / "users.py", render_fastapi_user_route()),
43
43
  (models_dir / "__init__.py", render_pydantic_models_init()),
@@ -26,6 +26,8 @@ from .components import (
26
26
  render_sqlalchemy_model,
27
27
  render_cli_command,
28
28
  render_cli_command_simple,
29
+ render_cli_commands_init,
30
+ render_fastapi_routes_init_autodiscovery,
29
31
  )
30
32
  from .loader import render_template
31
33
 
@@ -57,5 +59,7 @@ __all__ = [
57
59
  "render_sqlalchemy_model",
58
60
  "render_cli_command",
59
61
  "render_cli_command_simple",
62
+ "render_cli_commands_init",
63
+ "render_fastapi_routes_init_autodiscovery",
60
64
  "render_template",
61
65
  ]
@@ -33,8 +33,8 @@ async def {{ command_name }}({{ params_signature }}):
33
33
 
34
34
  # Your command implementation here
35
35
  click.echo("{{ command_name }} executed!")
36
- {% if params_signature -%}
37
- {% for param in params_list -%}
36
+ {% if params_signature %}
37
+ {% for param in params_list %}
38
38
  click.echo(f" {{ param }}: {{'{'}}{{ param }}{{'}'}}")
39
- {% endfor -%}
40
- {% endif %}
39
+ {% endfor %}
40
+ {% endif %}
@@ -12,8 +12,8 @@ import click
12
12
  def {{ command_name }}({{ params_signature }}):
13
13
  """{{ description }}"""
14
14
  click.echo("{{ command_name }} executed!")
15
- {% if params_signature -%}
16
- {% for param in params_list -%}
15
+ {% if params_signature %}
16
+ {% for param in params_list %}
17
17
  click.echo(f" {{ param }}: {{'{'}}{{ param }}{{'}'}}")
18
- {% endfor -%}
19
- {% endif %}
18
+ {% endfor %}
19
+ {% endif %}
@@ -0,0 +1,12 @@
1
+ """Custom CLI commands auto-discovery"""
2
+ from vega.discovery import discover_commands
3
+
4
+
5
+ def get_commands():
6
+ """
7
+ Auto-discover Click commands in this directory.
8
+
9
+ Returns:
10
+ list: List of Click command objects found in this directory
11
+ """
12
+ return discover_commands(__package__)
@@ -237,3 +237,13 @@ def render_cli_command_simple(
237
237
  params_signature=params_signature,
238
238
  params_list=params_list,
239
239
  )
240
+
241
+
242
+ def render_cli_commands_init() -> str:
243
+ """Return the template for cli/commands/__init__.py with auto-discovery"""
244
+ return render_template("commands_init.py.j2", subfolder="cli")
245
+
246
+
247
+ def render_fastapi_routes_init_autodiscovery() -> str:
248
+ """Return the template for web/routes/__init__.py with auto-discovery"""
249
+ return render_template("routes_init_autodiscovery.py.j2", subfolder="web")
@@ -35,7 +35,14 @@ def hello():
35
35
  click.echo("Add your CLI commands in presentation/cli/commands/")
36
36
 
37
37
 
38
- # Add more CLI commands here or import them from presentation/cli/commands/
38
+ # Auto-register custom commands from presentation/cli/commands/
39
+ try:
40
+ from presentation.cli.commands import get_commands
41
+ for cmd in get_commands():
42
+ cli.add_command(cmd)
43
+ except ImportError:
44
+ # cli/commands/ not created yet
45
+ pass
39
46
 
40
47
 
41
48
  if __name__ == "__main__":
@@ -88,7 +95,14 @@ def greet(name: str):
88
95
  # )
89
96
 
90
97
 
91
- # Add more CLI commands here or import them from presentation/cli/commands/
98
+ # Auto-register custom commands from presentation/cli/commands/
99
+ try:
100
+ from presentation.cli.commands import get_commands
101
+ for cmd in get_commands():
102
+ cli.add_command(cmd)
103
+ except ImportError:
104
+ # cli/commands/ not created yet
105
+ pass
92
106
 
93
107
 
94
108
  if __name__ == "__main__":
@@ -62,7 +62,14 @@ async def create_user(name: str, email: str):
62
62
  click.echo("Note: Replace this with your actual CreateUser interactor")
63
63
 
64
64
 
65
- # Add more CLI commands here or import them from presentation/cli/commands/
65
+ # Auto-register custom commands from presentation/cli/commands/
66
+ try:
67
+ from presentation.cli.commands import get_commands
68
+ for cmd in get_commands():
69
+ cli.add_command(cmd)
70
+ except ImportError:
71
+ # cli/commands/ not created yet
72
+ pass
66
73
 
67
74
 
68
75
  if __name__ == "__main__":
@@ -70,7 +70,14 @@ async def create_user(name: str, email: str):
70
70
  # )
71
71
 
72
72
 
73
- # Add more CLI commands here or import them from presentation/cli/commands/
73
+ # Auto-register custom commands from presentation/cli/commands/
74
+ try:
75
+ from presentation.cli.commands import get_commands
76
+ for cmd in get_commands():
77
+ cli.add_command(cmd)
78
+ except ImportError:
79
+ # cli/commands/ not created yet
80
+ pass
74
81
 
75
82
 
76
83
  if __name__ == "__main__":
@@ -0,0 +1,12 @@
1
+ """API routers auto-discovery"""
2
+ from vega.discovery import discover_routers
3
+
4
+
5
+ def get_api_router():
6
+ """
7
+ Auto-discover and register FastAPI routers in this directory.
8
+
9
+ Returns:
10
+ APIRouter: Main API router with all discovered routers included
11
+ """
12
+ return discover_routers(__package__)
@@ -0,0 +1,5 @@
1
+ """Auto-discovery utilities for Vega framework"""
2
+ from .routes import discover_routers
3
+ from .commands import discover_commands
4
+
5
+ __all__ = ["discover_routers", "discover_commands"]
@@ -0,0 +1,104 @@
1
+ """Click CLI commands auto-discovery utilities"""
2
+ import importlib
3
+ import inspect
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import List
7
+
8
+ try:
9
+ import click
10
+ except ImportError:
11
+ click = None
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def discover_commands(
17
+ base_package: str,
18
+ commands_subpackage: str = "presentation.cli.commands"
19
+ ) -> List["click.Command"]:
20
+ """
21
+ Auto-discover Click commands from a package.
22
+
23
+ This function scans a package directory for Python modules containing
24
+ Click Command instances and returns them as a list.
25
+
26
+ Args:
27
+ base_package: Base package name (use __package__ from calling module)
28
+ commands_subpackage: Subpackage path containing commands (default: "presentation.cli.commands")
29
+
30
+ Returns:
31
+ List[click.Command]: List of discovered Click commands
32
+
33
+ Example:
34
+ # In your project's presentation/cli/commands/__init__.py
35
+ from vega.discovery import discover_commands
36
+
37
+ def get_commands():
38
+ return discover_commands(__package__)
39
+
40
+ # Or with custom configuration
41
+ def get_commands():
42
+ return discover_commands(
43
+ __package__,
44
+ commands_subpackage="cli.custom_commands"
45
+ )
46
+
47
+ Note:
48
+ Each command module can export multiple Click Command instances.
49
+ All public (non-underscore prefixed) Command instances will be discovered.
50
+ """
51
+ if click is None:
52
+ raise ImportError(
53
+ "Click is not installed. Install it with: pip install click"
54
+ )
55
+
56
+ commands = []
57
+
58
+ # Resolve the commands package path
59
+ try:
60
+ # Determine the package to scan
61
+ if base_package.endswith(commands_subpackage):
62
+ commands_package = base_package
63
+ else:
64
+ # Extract base from fully qualified package name
65
+ parts = base_package.split('.')
66
+ # Find the root package (usually the project name)
67
+ root_package = parts[0]
68
+ commands_package = f"{root_package}.{commands_subpackage}"
69
+
70
+ # Import the commands package to get its path
71
+ commands_module = importlib.import_module(commands_package)
72
+ commands_dir = Path(commands_module.__file__).parent
73
+
74
+ logger.debug(f"Discovering commands in: {commands_dir}")
75
+
76
+ # Scan for command modules
77
+ discovered_count = 0
78
+ for file in commands_dir.glob("*.py"):
79
+ if file.stem == "__init__":
80
+ continue
81
+
82
+ module_name = f"{commands_package}.{file.stem}"
83
+
84
+ try:
85
+ module = importlib.import_module(module_name)
86
+
87
+ # Find all Click Command instances
88
+ for name, obj in inspect.getmembers(module):
89
+ if isinstance(obj, click.Command) and not name.startswith("_"):
90
+ commands.append(obj)
91
+ discovered_count += 1
92
+ logger.info(f"Registered command: {name} from {module_name}")
93
+
94
+ except Exception as e:
95
+ logger.warning(f"Failed to import {module_name}: {e}")
96
+ continue
97
+
98
+ logger.info(f"Auto-discovery complete: {discovered_count} command(s) registered")
99
+
100
+ except ImportError as e:
101
+ logger.error(f"Failed to import commands package '{commands_package}': {e}")
102
+ raise
103
+
104
+ return commands
@@ -0,0 +1,129 @@
1
+ """FastAPI router auto-discovery utilities"""
2
+ import importlib
3
+ import inspect
4
+ import logging
5
+ from pathlib import Path
6
+ from typing import Optional
7
+
8
+ try:
9
+ from fastapi import APIRouter
10
+ except ImportError:
11
+ APIRouter = None
12
+
13
+ logger = logging.getLogger(__name__)
14
+
15
+
16
+ def discover_routers(
17
+ base_package: str,
18
+ routes_subpackage: str = "presentation.web.routes",
19
+ api_prefix: str = "/api",
20
+ auto_tags: bool = True,
21
+ auto_prefix: bool = True
22
+ ) -> "APIRouter":
23
+ """
24
+ Auto-discover and register FastAPI routers from a package.
25
+
26
+ This function scans a package directory for Python modules containing
27
+ APIRouter instances named 'router' and automatically registers them
28
+ with the main router.
29
+
30
+ Args:
31
+ base_package: Base package name (use __package__ from calling module)
32
+ routes_subpackage: Subpackage path containing routes (default: "presentation.web.routes")
33
+ api_prefix: Prefix for the main API router (default: "/api")
34
+ auto_tags: Automatically generate tags from module name (default: True)
35
+ auto_prefix: Automatically generate prefix from module name (default: True)
36
+
37
+ Returns:
38
+ APIRouter: Main router with all discovered routers included
39
+
40
+ Example:
41
+ # In your project's presentation/web/routes/__init__.py
42
+ from vega.discovery import discover_routers
43
+
44
+ router = discover_routers(__package__)
45
+
46
+ # Or with custom configuration
47
+ router = discover_routers(
48
+ __package__,
49
+ routes_subpackage="api.routes",
50
+ api_prefix="/v1"
51
+ )
52
+
53
+ Note:
54
+ Each route module should export an APIRouter instance named 'router'.
55
+ The module filename will be used for tags and prefix generation if enabled.
56
+ """
57
+ if APIRouter is None:
58
+ raise ImportError(
59
+ "FastAPI is not installed. Install it with: pip install fastapi"
60
+ )
61
+
62
+ main_router = APIRouter(prefix=api_prefix)
63
+
64
+ # Resolve the routes package path
65
+ try:
66
+ # Determine the package to scan
67
+ if base_package.endswith(routes_subpackage):
68
+ routes_package = base_package
69
+ else:
70
+ # Extract base from fully qualified package name
71
+ parts = base_package.split('.')
72
+ # Find the root package (usually the project name)
73
+ root_package = parts[0]
74
+ routes_package = f"{root_package}.{routes_subpackage}"
75
+
76
+ # Import the routes package to get its path
77
+ routes_module = importlib.import_module(routes_package)
78
+ routes_dir = Path(routes_module.__file__).parent
79
+
80
+ logger.debug(f"Discovering routers in: {routes_dir}")
81
+
82
+ # Scan for router modules
83
+ discovered_count = 0
84
+ for file in routes_dir.glob("*.py"):
85
+ if file.stem == "__init__":
86
+ continue
87
+
88
+ module_name = f"{routes_package}.{file.stem}"
89
+
90
+ try:
91
+ module = importlib.import_module(module_name)
92
+
93
+ # Find APIRouter instance named 'router'
94
+ router = getattr(module, 'router', None)
95
+
96
+ if isinstance(router, APIRouter):
97
+ # Generate tags and prefix from module name
98
+ if auto_tags:
99
+ tag = file.stem.replace("_", " ").title()
100
+ tags = [tag]
101
+ else:
102
+ tags = None
103
+
104
+ if auto_prefix:
105
+ prefix = f"/{file.stem.replace('_', '-')}"
106
+ else:
107
+ prefix = None
108
+
109
+ main_router.include_router(
110
+ router,
111
+ tags=tags,
112
+ prefix=prefix
113
+ )
114
+ discovered_count += 1
115
+ logger.info(f"Registered router: {module_name} (tags={tags}, prefix={prefix})")
116
+ else:
117
+ logger.debug(f"No 'router' found in {module_name}")
118
+
119
+ except Exception as e:
120
+ logger.warning(f"Failed to import {module_name}: {e}")
121
+ continue
122
+
123
+ logger.info(f"Auto-discovery complete: {discovered_count} router(s) registered")
124
+
125
+ except ImportError as e:
126
+ logger.error(f"Failed to import routes package '{routes_package}': {e}")
127
+ raise
128
+
129
+ return main_router
File without changes