vega-framework 0.1.29__py3-none-any.whl → 0.1.30__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.
@@ -15,6 +15,8 @@ from vega.cli.templates import (
15
15
  render_sqlalchemy_model,
16
16
  render_cli_command,
17
17
  render_cli_command_simple,
18
+ render_event,
19
+ render_event_handler,
18
20
  render_template,
19
21
  )
20
22
  from vega.cli.scaffolds import create_fastapi_scaffold
@@ -61,6 +63,11 @@ def generate_component(
61
63
  class_name = to_pascal_case(name)
62
64
  implementation = implementation.strip() if implementation else None
63
65
 
66
+ if component_type == 'repo':
67
+ component_type = 'repository'
68
+ if component_type in {'event-handler', 'subscriber'}:
69
+ component_type = 'event_handler'
70
+
64
71
  suffixes = {
65
72
  "repository": "Repository",
66
73
  "service": "Service",
@@ -105,6 +112,10 @@ def generate_component(
105
112
  _generate_web_models(project_root, project_name, name, is_request, is_response)
106
113
  elif component_type == 'command':
107
114
  _generate_command(project_root, project_name, name, implementation)
115
+ elif component_type == 'event':
116
+ _generate_event(project_root, project_name, class_name, file_name)
117
+ elif component_type == 'event_handler':
118
+ _generate_event_handler(project_root, project_name, class_name, file_name)
108
119
 
109
120
 
110
121
  def _generate_entity(project_root: Path, project_name: str, class_name: str, file_name: str):
@@ -875,3 +886,106 @@ def _generate_command(project_root: Path, project_name: str, name: str, is_async
875
886
  click.echo(click.style(f" (Commands are auto-discovered from cli/commands/)", fg='bright_black'))
876
887
  if with_interactor:
877
888
  click.echo(f" 3. Create interactor: vega generate interactor {interactor_name}")
889
+
890
+
891
+ def _generate_event(project_root: Path, project_name: str, class_name: str, file_name: str):
892
+ """Generate a domain event."""
893
+
894
+ events_path = project_root / "domain" / "events"
895
+ events_path.mkdir(parents=True, exist_ok=True)
896
+
897
+ init_file = events_path / "__init__.py"
898
+ if not init_file.exists():
899
+ init_file.write_text("")
900
+
901
+ file_path = events_path / f"{file_name}.py"
902
+ if file_path.exists():
903
+ click.echo(click.style(f"ERROR: Error: {file_path.relative_to(project_root)} already exists", fg='red'))
904
+ return
905
+
906
+ click.echo("\nDefine event payload fields (press Enter to skip):")
907
+ fields: list[dict[str, str]] = []
908
+ while True:
909
+ field_name = click.prompt("Field name", default="", show_default=False)
910
+ if not field_name:
911
+ break
912
+ snake_name = to_snake_case(field_name)
913
+ type_hint = click.prompt("Type hint", default="str")
914
+ description = click.prompt(
915
+ "Description",
916
+ default=f"{snake_name.replace('_', ' ').capitalize()} value",
917
+ )
918
+ fields.append(
919
+ {
920
+ "name": snake_name,
921
+ "type_hint": type_hint,
922
+ "description": description,
923
+ }
924
+ )
925
+
926
+ content = render_event(class_name, fields)
927
+ file_path.write_text(content)
928
+ click.echo(f"+ Created {click.style(str(file_path.relative_to(project_root)), fg='green')}")
929
+
930
+ click.echo("\nNext steps:")
931
+ click.echo(" 1. Publish the event from your domain logic.")
932
+ click.echo(" 2. Generate subscribers: vega generate subscriber <HandlerName>")
933
+
934
+
935
+ def _generate_event_handler(project_root: Path, project_name: str, class_name: str, file_name: str):
936
+ """Generate an application-level event handler/subscriber."""
937
+
938
+ handlers_path = project_root / "application" / "events"
939
+ handlers_path.mkdir(parents=True, exist_ok=True)
940
+
941
+ init_file = handlers_path / "__init__.py"
942
+ if not init_file.exists():
943
+ init_file.write_text("")
944
+
945
+ handler_file = handlers_path / f"{file_name}.py"
946
+ if handler_file.exists():
947
+ click.echo(click.style(f"ERROR: Error: {handler_file.relative_to(project_root)} already exists", fg='red'))
948
+ return
949
+
950
+ default_event_class = class_name
951
+ if default_event_class.lower().endswith("handler"):
952
+ default_event_class = default_event_class[:-7] or class_name
953
+
954
+ event_class = click.prompt("Event class name", default=default_event_class)
955
+ event_module_default = f"domain.events.{to_snake_case(event_class)}"
956
+ event_module = click.prompt("Event module path", default=event_module_default)
957
+
958
+ priority = click.prompt("Handler priority (higher runs first)", default=0, type=int)
959
+ retry_on_error = click.confirm("Retry on failure?", default=False)
960
+ max_retries = None
961
+ if retry_on_error:
962
+ max_retries = click.prompt("Max retries", default=3, type=int)
963
+
964
+ decorator_args = event_class
965
+ options: list[str] = []
966
+ if priority:
967
+ options.append(f"priority={priority}")
968
+ if retry_on_error:
969
+ options.append("retry_on_error=True")
970
+ if max_retries is not None:
971
+ options.append(f"max_retries={max_retries}")
972
+ if options:
973
+ decorator_args = f"{event_class}, " + ", ".join(options)
974
+
975
+ handler_func_name = to_snake_case(class_name)
976
+
977
+ content = render_event_handler(
978
+ class_name=class_name,
979
+ handler_func_name=handler_func_name,
980
+ event_name=event_class,
981
+ event_module=event_module,
982
+ decorator_args=decorator_args,
983
+ )
984
+
985
+ handler_file.write_text(content)
986
+ click.echo(f"+ Created {click.style(str(handler_file.relative_to(project_root)), fg='green')}")
987
+
988
+ click.echo("\nNext steps:")
989
+ click.echo(f" 1. Implement your handler in {handler_file.relative_to(project_root)}")
990
+ click.echo(" 2. Ensure the module is imported during application bootstrap (autodiscovery or manual import).")
991
+ click.echo(" 3. Run your workflow and verify the subscriber reacts to the event.")
vega/cli/commands/init.py CHANGED
@@ -38,6 +38,7 @@ def init_project(project_name: str, template: str, parent_path: str):
38
38
  "infrastructure/repositories",
39
39
  "infrastructure/services",
40
40
  "presentation/cli/commands",
41
+ "events",
41
42
  "tests/domain",
42
43
  "tests/application",
43
44
  "tests/infrastructure",
@@ -53,6 +54,11 @@ def init_project(project_name: str, template: str, parent_path: str):
53
54
  from vega.cli.templates import render_cli_commands_init
54
55
  content = render_cli_commands_init()
55
56
  (dir_path / "__init__.py").write_text(content)
57
+ # Use auto-discovery template for events/
58
+ elif directory == "events":
59
+ from vega.cli.templates import render_events_init
60
+ content = render_events_init()
61
+ (dir_path / "__init__.py").write_text(content)
56
62
 
57
63
  click.echo(f" + Created {directory}/")
58
64
 
vega/cli/main.py CHANGED
@@ -57,7 +57,20 @@ def init(project_name, template, path):
57
57
 
58
58
  @cli.command()
59
59
  @click.argument('component_type', type=click.Choice([
60
- 'entity', 'repository', 'repo', 'service', 'interactor', 'mediator', 'router', 'middleware', 'webmodel', 'model', 'command'
60
+ 'entity',
61
+ 'repository',
62
+ 'repo',
63
+ 'service',
64
+ 'interactor',
65
+ 'mediator',
66
+ 'router',
67
+ 'middleware',
68
+ 'webmodel',
69
+ 'model',
70
+ 'command',
71
+ 'event',
72
+ 'event-handler',
73
+ 'subscriber',
61
74
  ]))
62
75
  @click.argument('name')
63
76
  @click.option('--path', default='.', help='Project root path')
@@ -80,6 +93,8 @@ def generate(component_type, name, path, impl, request, response):
80
93
  webmodel - Pydantic request/response models (requires web module)
81
94
  model - SQLAlchemy model (requires sqlalchemy module)
82
95
  command - CLI command (async by default)
96
+ event - Domain event (immutable dataclass + metadata)
97
+ event-handler/subscriber - Application-level event subscriber
83
98
 
84
99
  Examples:
85
100
  vega generate entity Product
@@ -94,6 +109,8 @@ def generate(component_type, name, path, impl, request, response):
94
109
  vega generate model User
95
110
  vega generate command CreateUser
96
111
  vega generate command ListUsers --impl sync
112
+ vega generate event UserCreated
113
+ vega generate subscriber SendWelcomeEmail --name UserCreated
97
114
  """
98
115
  # Normalize 'repo' to 'repository'
99
116
  if component_type == 'repo':
@@ -28,6 +28,9 @@ from .components import (
28
28
  render_cli_command_simple,
29
29
  render_cli_commands_init,
30
30
  render_fastapi_routes_init_autodiscovery,
31
+ render_event,
32
+ render_event_handler,
33
+ render_events_init,
31
34
  )
32
35
  from .loader import render_template
33
36
 
@@ -61,5 +64,8 @@ __all__ = [
61
64
  "render_cli_command_simple",
62
65
  "render_cli_commands_init",
63
66
  "render_fastapi_routes_init_autodiscovery",
67
+ "render_event",
68
+ "render_event_handler",
69
+ "render_events_init",
64
70
  "render_template",
65
71
  ]
@@ -247,3 +247,37 @@ def render_cli_commands_init() -> str:
247
247
  def render_fastapi_routes_init_autodiscovery() -> str:
248
248
  """Return the template for web/routes/__init__.py with auto-discovery"""
249
249
  return render_template("routes_init_autodiscovery.py.j2", subfolder="web")
250
+
251
+
252
+ def render_event(class_name: str, fields: list[dict]) -> str:
253
+ """Return the template for a domain event"""
254
+ return render_template(
255
+ "event.py.j2",
256
+ subfolder="domain",
257
+ class_name=class_name,
258
+ fields=fields,
259
+ )
260
+
261
+
262
+ def render_event_handler(
263
+ class_name: str,
264
+ handler_func_name: str,
265
+ event_name: str,
266
+ event_module: str,
267
+ decorator_args: str,
268
+ ) -> str:
269
+ """Return the template for an event handler"""
270
+ return render_template(
271
+ "event_handler.py.j2",
272
+ subfolder="domain",
273
+ class_name=class_name,
274
+ handler_func_name=handler_func_name,
275
+ event_name=event_name,
276
+ event_module=event_module,
277
+ decorator_args=decorator_args,
278
+ )
279
+
280
+
281
+ def render_events_init() -> str:
282
+ """Return the template for events/__init__.py with auto-discovery"""
283
+ return render_template("events_init.py.j2", subfolder="project")
@@ -0,0 +1,23 @@
1
+ """{{ class_name }} domain event"""
2
+ from dataclasses import dataclass
3
+ from vega.events import Event
4
+
5
+
6
+ @dataclass(frozen=True)
7
+ class {{ class_name }}(Event):
8
+ """
9
+ {{ class_name }} event.
10
+
11
+ This event is published when {{ class_name|lower|replace('created', 'is created')|replace('updated', 'is updated')|replace('deleted', 'is deleted') }}.
12
+
13
+ Attributes:
14
+ {% for field in fields %} {{ field.name }}: {{ field.type_hint }} - {{ field.description }}
15
+ {% endfor %}
16
+ """
17
+ {% for field in fields %}
18
+ {{ field.name }}: {{ field.type_hint }}
19
+ {% endfor %}
20
+
21
+ def __post_init__(self):
22
+ """Initialize event metadata"""
23
+ super().__init__()
@@ -0,0 +1,22 @@
1
+ """{{ class_name }} event handler"""
2
+ from vega.events import subscribe
3
+ from {{ event_module }} import {{ event_name }}
4
+
5
+
6
+ @subscribe({{ decorator_args }})
7
+ async def {{ handler_func_name }}(event: {{ event_name }}):
8
+ """
9
+ Handle {{ event_name }} event.
10
+
11
+ This handler is automatically called when a {{ event_name }} event is published.
12
+
13
+ Args:
14
+ event: The {{ event_name }} event instance
15
+
16
+ Example:
17
+ # Event is automatically published from domain logic
18
+ # This handler will be called automatically
19
+ pass
20
+ """
21
+ # TODO: Implement event handling logic
22
+ raise NotImplementedError("Implement {{ class_name }} handler")
@@ -3,7 +3,7 @@ from abc import abstractmethod
3
3
  from typing import List, Optional
4
4
 
5
5
  from vega.patterns import Repository
6
- from entities.{{ entity_file }} import {{ entity_name }}
6
+ from domain.entities.{{ entity_file }} import {{ entity_name }}
7
7
 
8
8
 
9
9
  class {{ class_name }}(Repository[{{ entity_name }}]):
@@ -0,0 +1,32 @@
1
+ """Event handlers auto-discovery
2
+
3
+ This module automatically discovers and registers all event handlers
4
+ decorated with @subscribe() in this directory.
5
+
6
+ The auto-discovery happens when you call register_all_handlers().
7
+ This should be called during application startup.
8
+ """
9
+ from vega.discovery import discover_event_handlers
10
+
11
+
12
+ def register_all_handlers():
13
+ """
14
+ Auto-discover and register all event handlers in this directory.
15
+
16
+ This function scans all Python modules in the events/ directory and
17
+ imports them to trigger @subscribe() decorator registration.
18
+
19
+ Call this function during application bootstrap to ensure all event
20
+ handlers are registered with the global event bus.
21
+
22
+ Example:
23
+ # In your main.py or application startup
24
+ from events import register_all_handlers
25
+
26
+ # Register all event handlers
27
+ register_all_handlers()
28
+
29
+ # Now events will be handled automatically
30
+ await UserCreated(user_id="123", email="test@test.com", name="John")
31
+ """
32
+ discover_event_handlers(__package__)
@@ -1,5 +1,6 @@
1
1
  """Auto-discovery utilities for Vega framework"""
2
2
  from .routes import discover_routers
3
3
  from .commands import discover_commands
4
+ from .events import discover_event_handlers
4
5
 
5
- __all__ = ["discover_routers", "discover_commands"]
6
+ __all__ = ["discover_routers", "discover_commands", "discover_event_handlers"]
@@ -0,0 +1,86 @@
1
+ """Event handlers auto-discovery utilities"""
2
+ import importlib
3
+ import logging
4
+ from pathlib import Path
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+
9
+ def discover_event_handlers(
10
+ base_package: str,
11
+ events_subpackage: str = "events"
12
+ ) -> None:
13
+ """
14
+ Auto-discover and register event handlers from a package.
15
+
16
+ This function scans a package directory for Python modules containing
17
+ event handlers decorated with @subscribe() and automatically imports them
18
+ to trigger registration with the global event bus.
19
+
20
+ Args:
21
+ base_package: Base package name (use __package__ from calling module)
22
+ events_subpackage: Subpackage path containing events (default: "events")
23
+
24
+ Example:
25
+ # In your project's events/__init__.py
26
+ from vega.discovery import discover_event_handlers
27
+
28
+ def register_all_handlers():
29
+ discover_event_handlers(__package__)
30
+
31
+ # Or with custom configuration
32
+ def register_all_handlers():
33
+ discover_event_handlers(
34
+ __package__,
35
+ events_subpackage="application.events"
36
+ )
37
+
38
+ Note:
39
+ Event handlers are registered automatically when modules are imported.
40
+ This function simply imports all modules in the events directory to
41
+ trigger the @subscribe() decorator registration.
42
+
43
+ The function doesn't return anything - handlers register themselves
44
+ with the global event bus via the @subscribe() decorator.
45
+ """
46
+ # Resolve the events package path
47
+ try:
48
+ # Determine the package to scan
49
+ if base_package.endswith(events_subpackage):
50
+ events_package = base_package
51
+ else:
52
+ # Extract base from fully qualified package name
53
+ parts = base_package.split('.')
54
+ # Find the root package (usually the project name)
55
+ root_package = parts[0]
56
+ events_package = f"{root_package}.{events_subpackage}"
57
+
58
+ # Import the events package to get its path
59
+ events_module = importlib.import_module(events_package)
60
+ events_dir = Path(events_module.__file__).parent
61
+
62
+ logger.debug(f"Discovering event handlers in: {events_dir}")
63
+
64
+ # Scan for event handler modules
65
+ discovered_count = 0
66
+ for file in events_dir.glob("*.py"):
67
+ if file.stem == "__init__":
68
+ continue
69
+
70
+ module_name = f"{events_package}.{file.stem}"
71
+
72
+ try:
73
+ # Import the module to trigger @subscribe() decorator registration
74
+ importlib.import_module(module_name)
75
+ discovered_count += 1
76
+ logger.info(f"Loaded event handlers from: {module_name}")
77
+
78
+ except Exception as e:
79
+ logger.warning(f"Failed to import {module_name}: {e}")
80
+ continue
81
+
82
+ logger.info(f"Auto-discovery complete: {discovered_count} event module(s) loaded")
83
+
84
+ except ImportError as e:
85
+ logger.error(f"Failed to import events package '{events_package}': {e}")
86
+ raise