vega-framework 0.1.15__py3-none-any.whl → 0.1.17__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.
vega/cli/__init__.py CHANGED
@@ -1,5 +1,11 @@
1
1
  """Vega Framework CLI tools"""
2
2
 
3
- from vega.cli.main import cli
3
+ # Lazy import to avoid circular dependencies when importing utilities
4
+ def __getattr__(name: str):
5
+ if name == "cli":
6
+ from vega.cli.main import cli
7
+ return cli
8
+ raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
9
+
4
10
 
5
11
  __all__ = ["cli"]
@@ -14,6 +14,8 @@ from vega.cli.templates import (
14
14
  render_fastapi_router,
15
15
  render_fastapi_middleware,
16
16
  render_sqlalchemy_model,
17
+ render_cli_command,
18
+ render_cli_command_simple,
17
19
  )
18
20
  from vega.cli.scaffolds import create_fastapi_scaffold
19
21
 
@@ -124,6 +126,8 @@ def generate_component(
124
126
  _generate_middleware(project_root, project_name, class_name, file_name)
125
127
  elif component_type == 'model':
126
128
  _generate_sqlalchemy_model(project_root, project_name, class_name, file_name)
129
+ elif component_type == 'command':
130
+ _generate_command(project_root, project_name, name, implementation)
127
131
 
128
132
 
129
133
  def _generate_entity(project_root: Path, project_name: str, class_name: str, file_name: str):
@@ -670,3 +674,137 @@ from infrastructure.models.{file_name} import {class_name}Model # noqa: F401
670
674
  click.echo(click.style(f'''
671
675
  from infrastructure.models.{file_name} import {class_name}Model # noqa: F401
672
676
  ''', fg='cyan'))
677
+
678
+
679
+ def _generate_command(project_root: Path, project_name: str, name: str, is_async: str | None = None) -> None:
680
+ """Generate a CLI command"""
681
+
682
+ # Check if presentation/cli exists
683
+ cli_path = project_root / "presentation" / "cli"
684
+ if not cli_path.exists():
685
+ cli_path.mkdir(parents=True, exist_ok=True)
686
+ click.echo(f"+ Created {click.style(str(cli_path.relative_to(project_root)), fg='green')}")
687
+
688
+ # Create commands directory if it doesn't exist
689
+ commands_path = cli_path / "commands"
690
+ commands_path.mkdir(exist_ok=True)
691
+
692
+ # Check if __init__.py exists
693
+ init_file = commands_path / "__init__.py"
694
+ if not init_file.exists():
695
+ init_file.write_text('"""CLI Commands"""\n')
696
+ click.echo(f"+ Created {click.style(str(init_file.relative_to(project_root)), fg='green')}")
697
+
698
+ # Convert name to snake_case for command and file
699
+ command_name = to_snake_case(name).replace('_', '-')
700
+ file_name = to_snake_case(name)
701
+
702
+ # Generate command file
703
+ command_file = commands_path / f"{file_name}.py"
704
+
705
+ if command_file.exists():
706
+ click.echo(click.style(f"ERROR: Error: {command_file.relative_to(project_root)} already exists", fg='red'))
707
+ return
708
+
709
+ # Determine if async (default is async unless explicitly set to 'sync' or 'simple')
710
+ use_async = is_async not in ['sync', 'simple', 'false', 'no'] if is_async else True
711
+
712
+ # Prompt for command details
713
+ description = click.prompt("Command description", default=f"{name} command")
714
+
715
+ # Ask if user wants to add options/arguments
716
+ add_params = click.confirm("Add options or arguments?", default=False)
717
+
718
+ options = []
719
+ arguments = []
720
+ params_list = []
721
+
722
+ if add_params:
723
+ click.echo("\nAdd options (e.g., --name, --email). Press Enter when done.")
724
+ while True:
725
+ opt_name = click.prompt("Option name (without --)", default="", show_default=False)
726
+ if not opt_name:
727
+ break
728
+ opt_type = click.prompt("Type", default="str", type=click.Choice(['str', 'int', 'bool']))
729
+ opt_required = click.confirm("Required?", default=False)
730
+ opt_help = click.prompt("Help text", default=f"{opt_name.replace('-', ' ').replace('_', ' ')}")
731
+
732
+ params_list.append(opt_name.replace('-', '_'))
733
+
734
+ opt_params = f"help='{opt_help}'"
735
+ if opt_required:
736
+ opt_params += ", required=True"
737
+ if opt_type != 'str':
738
+ if opt_type == 'bool':
739
+ opt_params += ", is_flag=True"
740
+ else:
741
+ opt_params += f", type={opt_type}"
742
+
743
+ options.append({
744
+ "flag": f"--{opt_name}",
745
+ "params": opt_params
746
+ })
747
+
748
+ click.echo("\nAdd arguments (positional). Press Enter when done.")
749
+ while True:
750
+ arg_name = click.prompt("Argument name", default="", show_default=False)
751
+ if not arg_name:
752
+ break
753
+ arg_required = click.confirm("Required?", default=True)
754
+
755
+ params_list.append(arg_name)
756
+
757
+ arg_params = "" if arg_required else ", required=False"
758
+ arguments.append({
759
+ "name": arg_name,
760
+ "params": arg_params
761
+ })
762
+
763
+ params_signature = ", ".join(params_list) if params_list else ""
764
+
765
+ # Ask about interactor usage
766
+ with_interactor = False
767
+ interactor_name = ""
768
+ if use_async:
769
+ with_interactor = click.confirm("Will this command use an interactor?", default=True)
770
+ if with_interactor:
771
+ interactor_name = click.prompt("Interactor name", default=f"{to_pascal_case(name)}")
772
+
773
+ usage_example = f"python main.py {command_name}"
774
+ if params_list:
775
+ usage_example += " " + " ".join([f"--{p.replace('_', '-')}=value" if f"--{p.replace('_', '-')}" in str(options) else p for p in params_list])
776
+
777
+ # Generate content
778
+ if use_async:
779
+ content = render_cli_command(
780
+ command_name=file_name,
781
+ description=description,
782
+ options=options,
783
+ arguments=arguments,
784
+ params_signature=params_signature,
785
+ params_list=params_list,
786
+ with_interactor=with_interactor,
787
+ usage_example=usage_example,
788
+ interactor_name=interactor_name,
789
+ )
790
+ else:
791
+ content = render_cli_command_simple(
792
+ command_name=file_name,
793
+ description=description,
794
+ options=options,
795
+ arguments=arguments,
796
+ params_signature=params_signature,
797
+ params_list=params_list,
798
+ )
799
+
800
+ command_file.write_text(content)
801
+ click.echo(f"+ Created {click.style(str(command_file.relative_to(project_root)), fg='green')}")
802
+
803
+ # Instructions for next steps
804
+ click.echo(f"\nNext steps:")
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'))
809
+ if with_interactor:
810
+ click.echo(f" 3. Create interactor: vega generate interactor {interactor_name}")
@@ -107,9 +107,9 @@ def history():
107
107
  @migrate.command()
108
108
  def init():
109
109
  """Initialize database with current schema (create tables)"""
110
- import asyncio
111
110
  from pathlib import Path
112
111
  import sys
112
+ from vega.cli.utils import async_command
113
113
 
114
114
  # Add project root to path to allow imports
115
115
  project_root = Path.cwd()
@@ -123,13 +123,14 @@ def init():
123
123
  click.echo("Make sure you have SQLAlchemy configured in your project")
124
124
  sys.exit(1)
125
125
 
126
+ @async_command
126
127
  async def _init():
127
128
  click.echo("Creating database tables...")
128
129
  await db_manager.create_tables()
129
130
  click.secho("Database tables created successfully", fg='green')
130
131
 
131
132
  try:
132
- asyncio.run(_init())
133
+ _init()
133
134
  except Exception as e:
134
135
  click.secho(f"Failed to initialize database: {e}", fg='red')
135
136
  sys.exit(1)
vega/cli/main.py CHANGED
@@ -56,11 +56,11 @@ def init(project_name, template, path):
56
56
 
57
57
  @cli.command()
58
58
  @click.argument('component_type', type=click.Choice([
59
- 'entity', 'repository', 'repo', 'service', 'interactor', 'mediator', 'router', 'middleware', 'model'
59
+ 'entity', 'repository', 'repo', 'service', 'interactor', 'mediator', 'router', 'middleware', 'model', 'command'
60
60
  ]))
61
61
  @click.argument('name')
62
62
  @click.option('--path', default='.', help='Project root path')
63
- @click.option('--impl', default=None, help='Generate infrastructure implementation for repository/service (e.g., memory, sql)')
63
+ @click.option('--impl', default=None, help='Generate infrastructure implementation for repository/service (e.g., memory, sql) or command type (async, sync)')
64
64
  def generate(component_type, name, path, impl):
65
65
  """
66
66
  Generate a component in your Vega project.
@@ -75,6 +75,7 @@ def generate(component_type, name, path, impl):
75
75
  router - FastAPI router (requires web module)
76
76
  middleware - FastAPI middleware (requires web module)
77
77
  model - SQLAlchemy model (requires sqlalchemy module)
78
+ command - CLI command (async by default)
78
79
 
79
80
  Examples:
80
81
  vega generate entity Product
@@ -85,6 +86,8 @@ def generate(component_type, name, path, impl):
85
86
  vega generate router Product
86
87
  vega generate middleware Logging
87
88
  vega generate model User
89
+ vega generate command CreateUser
90
+ vega generate command ListUsers --impl sync
88
91
  """
89
92
  # Normalize 'repo' to 'repository'
90
93
  if component_type == 'repo':
@@ -24,6 +24,8 @@ from .components import (
24
24
  render_alembic_env,
25
25
  render_alembic_script_mako,
26
26
  render_sqlalchemy_model,
27
+ render_cli_command,
28
+ render_cli_command_simple,
27
29
  )
28
30
  from .loader import render_template
29
31
 
@@ -53,5 +55,7 @@ __all__ = [
53
55
  "render_alembic_env",
54
56
  "render_alembic_script_mako",
55
57
  "render_sqlalchemy_model",
58
+ "render_cli_command",
59
+ "render_cli_command_simple",
56
60
  "render_template",
57
61
  ]
@@ -0,0 +1,40 @@
1
+ """{{ description }}"""
2
+ import click
3
+ from vega.cli.utils import async_command
4
+ import config # noqa: F401 - Initialize DI container
5
+
6
+ {% if with_interactor -%}
7
+ # Import your interactors here
8
+ # from domain.interactors.{{ interactor_name }} import {{ interactor_name }}
9
+ {% endif %}
10
+
11
+ @click.command()
12
+ {%- for option in options %}
13
+ @click.option('{{ option.flag }}', {{ option.params }})
14
+ {%- endfor %}
15
+ {%- for argument in arguments %}
16
+ @click.argument('{{ argument.name }}'{{ argument.params }})
17
+ {%- endfor %}
18
+ @async_command
19
+ async def {{ command_name }}({{ params_signature }}):
20
+ """
21
+ {{ description }}
22
+
23
+ {% if usage_example -%}
24
+ Usage:
25
+ {{ usage_example }}
26
+ {% endif -%}
27
+ """
28
+ {% if with_interactor -%}
29
+ # Example: Execute an interactor
30
+ # result = await MyInteractor(param=value)
31
+ # click.echo(f"Result: {result}")
32
+ {% endif -%}
33
+
34
+ # Your command implementation here
35
+ click.echo("{{ command_name }} executed!")
36
+ {% if params_signature -%}
37
+ {% for param in params_list -%}
38
+ click.echo(f" {{ param }}: {{'{'}}{{ param }}{{'}'}}")
39
+ {% endfor -%}
40
+ {% endif %}
@@ -0,0 +1,19 @@
1
+ """{{ description }}"""
2
+ import click
3
+
4
+
5
+ @click.command()
6
+ {%- for option in options %}
7
+ @click.option('{{ option.flag }}', {{ option.params }})
8
+ {%- endfor %}
9
+ {%- for argument in arguments %}
10
+ @click.argument('{{ argument.name }}'{{ argument.params }})
11
+ {%- endfor %}
12
+ def {{ command_name }}({{ params_signature }}):
13
+ """{{ description }}"""
14
+ click.echo("{{ command_name }} executed!")
15
+ {% if params_signature -%}
16
+ {% for param in params_list -%}
17
+ click.echo(f" {{ param }}: {{'{'}}{{ param }}{{'}'}}")
18
+ {% endfor -%}
19
+ {% endif %}
@@ -189,3 +189,51 @@ def render_sqlalchemy_model(class_name: str, table_name: str) -> str:
189
189
  class_name=class_name,
190
190
  table_name=table_name,
191
191
  )
192
+
193
+
194
+ def render_cli_command(
195
+ command_name: str,
196
+ description: str,
197
+ options: list[dict],
198
+ arguments: list[dict],
199
+ params_signature: str,
200
+ params_list: list[str],
201
+ with_interactor: bool = True,
202
+ usage_example: str = "",
203
+ interactor_name: str = "",
204
+ ) -> str:
205
+ """Return the template for a CLI command"""
206
+ return render_template(
207
+ "command.py.j2",
208
+ subfolder="cli",
209
+ command_name=command_name,
210
+ description=description,
211
+ options=options,
212
+ arguments=arguments,
213
+ params_signature=params_signature,
214
+ params_list=params_list,
215
+ with_interactor=with_interactor,
216
+ usage_example=usage_example,
217
+ interactor_name=interactor_name,
218
+ )
219
+
220
+
221
+ def render_cli_command_simple(
222
+ command_name: str,
223
+ description: str,
224
+ options: list[dict],
225
+ arguments: list[dict],
226
+ params_signature: str,
227
+ params_list: list[str],
228
+ ) -> str:
229
+ """Return the template for a simple CLI command (non-async)"""
230
+ return render_template(
231
+ "command_simple.py.j2",
232
+ subfolder="cli",
233
+ command_name=command_name,
234
+ description=description,
235
+ options=options,
236
+ arguments=arguments,
237
+ params_signature=params_signature,
238
+ params_list=params_list,
239
+ )
@@ -262,6 +262,232 @@ class StripePaymentService(PaymentService):
262
262
  pass
263
263
  ```
264
264
 
265
+ ## Development Workflow with Vega CLI
266
+
267
+ Vega provides powerful CLI commands to quickly scaffold components following Clean Architecture principles. These commands help you maintain the correct architectural boundaries while accelerating development.
268
+
269
+ ### Generating Domain Layer Components
270
+
271
+ **Entities** - Pure business objects:
272
+ ```bash
273
+ vega generate entity User
274
+ vega generate entity Product
275
+ vega generate entity Order
276
+ ```
277
+
278
+ **Repository Interfaces** - Data persistence abstractions:
279
+ ```bash
280
+ vega generate repository UserRepository
281
+ vega generate repository Product # Auto-adds "Repository" suffix
282
+ ```
283
+
284
+ **Repository with Implementation** - Create both interface and concrete implementation:
285
+ ```bash
286
+ vega generate repository User --impl memory # In-memory implementation
287
+ vega generate repository User --impl sql # SQL implementation
288
+ vega generate repository User --impl postgres # PostgreSQL implementation
289
+ ```
290
+
291
+ **Service Interfaces** - External service abstractions:
292
+ ```bash
293
+ vega generate service EmailService
294
+ vega generate service PaymentService
295
+ ```
296
+
297
+ **Service with Implementation**:
298
+ ```bash
299
+ vega generate service Email --impl sendgrid
300
+ vega generate service Payment --impl stripe
301
+ ```
302
+
303
+ **Interactors** - Single-purpose use cases:
304
+ ```bash
305
+ vega generate interactor CreateUser
306
+ vega generate interactor GetUserById
307
+ vega generate interactor UpdateUserEmail
308
+ vega generate interactor DeleteUser
309
+ ```
310
+
311
+ ### Generating Application Layer Components
312
+
313
+ **Mediators** - Complex workflows:
314
+ ```bash
315
+ vega generate mediator UserRegistrationFlow
316
+ vega generate mediator CheckoutWorkflow
317
+ vega generate mediator OrderProcessingPipeline
318
+ ```
319
+
320
+ ### Generating Infrastructure Layer Components
321
+
322
+ **SQLAlchemy Models** - Database models (requires database support):
323
+ ```bash
324
+ vega generate model User
325
+ vega generate model Product
326
+ vega generate model Order
327
+ ```
328
+
329
+ Note: Models are automatically registered in Alembic for migrations.
330
+
331
+ ### Generating Presentation Layer Components
332
+
333
+ **CLI Commands** - Command-line interfaces:
334
+ ```bash
335
+ vega generate command CreateUser # Async command (default)
336
+ vega generate command ListUsers --impl sync # Synchronous command
337
+ ```
338
+
339
+ The generator will interactively prompt for:
340
+ - Command description
341
+ - Options and arguments (flags, parameters)
342
+ - Whether to use interactors
343
+ - Parameter types and validation
344
+
345
+ **FastAPI Routers** - HTTP API endpoints (requires web support):
346
+ ```bash
347
+ vega generate router User
348
+ vega generate router Product
349
+ vega generate router Order
350
+ ```
351
+
352
+ **FastAPI Middleware** - Request/response processing (requires web support):
353
+ ```bash
354
+ vega generate middleware Logging
355
+ vega generate middleware Authentication
356
+ vega generate middleware RateLimiting
357
+ ```
358
+
359
+ ### Adding Features to Existing Projects
360
+
361
+ **Add FastAPI Web Support**:
362
+ ```bash
363
+ vega add web
364
+ ```
365
+
366
+ Creates complete FastAPI scaffold:
367
+ - `presentation/web/` directory structure
368
+ - Routes and middleware setup
369
+ - Health check endpoints
370
+ - App factory pattern
371
+
372
+ **Add Database Support**:
373
+ ```bash
374
+ vega add sqlalchemy # or: vega add db
375
+ ```
376
+
377
+ Adds complete database infrastructure:
378
+ - SQLAlchemy async support
379
+ - Alembic migrations
380
+ - Database manager
381
+ - Base model classes
382
+
383
+ ### Database Migrations Workflow
384
+
385
+ After adding SQLAlchemy support, manage your database schema:
386
+
387
+ ```bash
388
+ # Initialize database (creates tables)
389
+ vega migrate init
390
+
391
+ # Create migration after model changes
392
+ vega migrate create -m "Add users table"
393
+ vega migrate create -m "Add email_verified field to users"
394
+
395
+ # Apply pending migrations
396
+ vega migrate upgrade
397
+
398
+ # Rollback last migration
399
+ vega migrate downgrade
400
+
401
+ # Check current migration status
402
+ vega migrate current
403
+
404
+ # View migration history
405
+ vega migrate history
406
+ ```
407
+
408
+ ### Project Validation
409
+
410
+ Validate your project structure and architecture compliance:
411
+
412
+ ```bash
413
+ vega doctor
414
+ vega doctor --path ./my-project
415
+ ```
416
+
417
+ Checks for:
418
+ - Correct folder structure
419
+ - DI container configuration
420
+ - Import dependencies
421
+ - Architecture violations (e.g., domain depending on infrastructure)
422
+
423
+ ### Development Best Practices with CLI
424
+
425
+ **1. Start with Domain Layer**:
426
+ ```bash
427
+ # Define your entities first
428
+ vega generate entity User
429
+
430
+ # Create repository interfaces
431
+ vega generate repository UserRepository
432
+
433
+ # Implement use cases
434
+ vega generate interactor CreateUser
435
+ vega generate interactor GetUserById
436
+ ```
437
+
438
+ **2. Add Infrastructure Implementations**:
439
+ ```bash
440
+ # Generate repository implementation
441
+ vega generate repository User --impl memory # Start with in-memory
442
+
443
+ # Later, add database support
444
+ vega add sqlalchemy
445
+ vega generate model User
446
+ vega generate repository User --impl sql
447
+ ```
448
+
449
+ **3. Build Application Workflows**:
450
+ ```bash
451
+ # Orchestrate multiple use cases
452
+ vega generate mediator UserRegistrationFlow
453
+ ```
454
+
455
+ **4. Create Delivery Mechanisms**:
456
+ ```bash
457
+ # CLI interface
458
+ vega generate command create-user
459
+
460
+ # Web API (add web first if not present)
461
+ vega add web
462
+ vega generate router User
463
+ ```
464
+
465
+ **5. Validate Architecture**:
466
+ ```bash
467
+ # Ensure clean architecture compliance
468
+ vega doctor
469
+ ```
470
+
471
+ ### CLI Quick Reference
472
+
473
+ | Command | Purpose |
474
+ |---------|---------|
475
+ | `vega init <name>` | Create new Vega project |
476
+ | `vega generate entity <Name>` | Generate domain entity |
477
+ | `vega generate repository <Name>` | Generate repository interface |
478
+ | `vega generate interactor <Name>` | Generate use case |
479
+ | `vega generate mediator <Name>` | Generate workflow |
480
+ | `vega generate command <Name>` | Generate CLI command |
481
+ | `vega generate router <Name>` | Generate FastAPI router |
482
+ | `vega generate model <Name>` | Generate SQLAlchemy model |
483
+ | `vega add web` | Add FastAPI support |
484
+ | `vega add sqlalchemy` | Add database support |
485
+ | `vega migrate <command>` | Manage database migrations |
486
+ | `vega doctor` | Validate project architecture |
487
+ | `vega update` | Update Vega Framework |
488
+
489
+ For complete documentation, see the [README](README.md#cli-commands).
490
+
265
491
  ## Dependency Injection
266
492
 
267
493
  Vega provides automatic dependency injection through decorators and a container.
@@ -507,7 +733,47 @@ class PriceCalculator:
507
733
  pass
508
734
  ```
509
735
 
510
- ### 6. Testing
736
+ ### 6. Async CLI Commands
737
+
738
+ Vega provides the `@async_command` decorator to use async/await in Click CLI commands, allowing you to execute interactors seamlessly:
739
+
740
+ ```python
741
+ import click
742
+ from vega.cli.utils import async_command
743
+
744
+ @click.command()
745
+ @click.option('--name', required=True)
746
+ @click.option('--email', required=True)
747
+ @async_command
748
+ async def create_user(name: str, email: str):
749
+ """Create a new user via CLI"""
750
+ # Import config to initialize DI container
751
+ import config # noqa: F401
752
+ from domain.interactors.create_user import CreateUser
753
+
754
+ # Execute async interactor
755
+ user = await CreateUser(name=name, email=email)
756
+ click.echo(f"Created user: {user.id} - {user.name}")
757
+ ```
758
+
759
+ **Benefits:**
760
+ - Execute async interactors directly in CLI commands
761
+ - Same business logic works in both CLI and Web (FastAPI) contexts
762
+ - Clean async/await syntax
763
+ - Automatic asyncio event loop management
764
+
765
+ **Alternative short syntax:**
766
+ ```python
767
+ from vega.cli.utils import coro # Alias for async_command
768
+
769
+ @click.command()
770
+ @coro
771
+ async def my_command():
772
+ result = await MyInteractor()
773
+ click.echo(result)
774
+ ```
775
+
776
+ ### 7. Testing
511
777
 
512
778
  Vega's architecture makes testing straightforward:
513
779