Skip to content

Commit 8443bc2

Browse files
authored
Merge pull request #142 from lbedner/handle-service-selections
Can Now Select Auth Service
2 parents ad42a6e + 10c1633 commit 8443bc2

17 files changed

Lines changed: 867 additions & 7 deletions

File tree

aegis/__main__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212

1313
from .commands.components import components_command
1414
from .commands.init import init_command
15+
from .commands.services import services_command
1516
from .commands.version import version_command
1617

1718
# Create the main Typer application
@@ -32,6 +33,7 @@
3233
# Register commands
3334
app.command(name="version")(version_command)
3435
app.command(name="components")(components_command)
36+
app.command(name="services")(services_command)
3537
app.command(name="init")(init_command)
3638

3739

aegis/cli/callbacks.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ def validate_and_resolve_components(
2424
if not value:
2525
return None
2626

27+
# Skip validation during help generation, but not for tests
28+
# Mock objects don't have a real resilient_parsing attribute set to True
29+
if hasattr(ctx, "resilient_parsing") and ctx.resilient_parsing is True:
30+
return None
31+
2732
# Parse comma-separated string
2833
components_raw = [c.strip() for c in value.split(",")]
2934

@@ -74,6 +79,11 @@ def validate_and_resolve_services(
7479
if not value:
7580
return None
7681

82+
# Skip validation during help generation, but not for tests
83+
# Mock objects don't have a real resilient_parsing attribute set to True
84+
if hasattr(ctx, "resilient_parsing") and ctx.resilient_parsing is True:
85+
return None
86+
7787
# Parse comma-separated string
7888
services_raw = [s.strip() for s in value.split(",")]
7989

aegis/commands/init.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -142,11 +142,11 @@ def init_command(
142142
clean_selected_only
143143
)
144144
if auto_added:
145-
typer.echo(f"\\n📦 Auto-added dependencies: {', '.join(auto_added)}")
145+
typer.echo(f"\n📦 Auto-added dependencies: {', '.join(auto_added)}")
146146

147147
# Create template generator with scheduler backend context
148148
template_gen = TemplateGenerator(
149-
project_name, list(selected_components), scheduler_backend
149+
project_name, list(selected_components), scheduler_backend, selected_services
150150
)
151151

152152
# Show selected configuration
@@ -176,28 +176,28 @@ def init_command(
176176
# Show template files that will be generated
177177
template_files = template_gen.get_template_files()
178178
if template_files:
179-
typer.echo("\\n📄 Component Files:")
179+
typer.echo("\n📄 Component Files:")
180180
for file_path in template_files:
181181
typer.echo(f" • {file_path}")
182182

183183
# Show entrypoints that will be created
184184
entrypoints = template_gen.get_entrypoints()
185185
if entrypoints:
186-
typer.echo("\\n🚀 Entrypoints:")
186+
typer.echo("\n🚀 Entrypoints:")
187187
for entrypoint in entrypoints:
188188
typer.echo(f" • {entrypoint}")
189189

190190
# Show worker queues that will be created
191191
worker_queues = template_gen.get_worker_queues()
192192
if worker_queues:
193-
typer.echo("\\n👷 Worker Queues:")
193+
typer.echo("\n👷 Worker Queues:")
194194
for queue in worker_queues:
195195
typer.echo(f" • {queue}")
196196

197197
# Show dependency information using template generator
198198
deps = template_gen._get_pyproject_deps()
199199
if deps:
200-
typer.echo("\\n📦 Dependencies to be installed:")
200+
typer.echo("\n📦 Dependencies to be installed:")
201201
for dep in deps:
202202
typer.echo(f" • {dep}")
203203

aegis/commands/services.py

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
"""
2+
Services command implementation.
3+
"""
4+
5+
import typer
6+
7+
from ..core.services import ServiceType, get_services_by_type
8+
9+
10+
def services_command() -> None:
11+
"""List available services and their dependencies."""
12+
13+
typer.echo("\n🔧 AVAILABLE SERVICES")
14+
typer.echo("=" * 40)
15+
16+
# Group services by type
17+
service_types = [
18+
(ServiceType.AUTH, "🔐 Authentication Services"),
19+
(ServiceType.PAYMENT, "💰 Payment Services"),
20+
(ServiceType.AI, "🤖 AI & Machine Learning Services"),
21+
(ServiceType.NOTIFICATION, "📧 Notification Services"),
22+
(ServiceType.ANALYTICS, "📊 Analytics Services"),
23+
(ServiceType.STORAGE, "💾 Storage Services"),
24+
]
25+
26+
services_found = False
27+
for service_type, header in service_types:
28+
type_services = get_services_by_type(service_type)
29+
if type_services:
30+
services_found = True
31+
typer.echo(f"\n{header}")
32+
typer.echo("-" * 40)
33+
34+
for name, spec in type_services.items():
35+
typer.echo(f" {name:12} - {spec.description}")
36+
if spec.required_components:
37+
typer.echo(
38+
f" Requires components: {', '.join(spec.required_components)}"
39+
)
40+
if spec.recommended_components:
41+
typer.echo(
42+
f" Recommends components: {', '.join(spec.recommended_components)}"
43+
)
44+
if spec.required_services:
45+
typer.echo(
46+
f" Requires services: {', '.join(spec.required_services)}"
47+
)
48+
49+
if not services_found:
50+
typer.echo(" No services available yet.")
51+
52+
typer.echo("\n💡 Use 'aegis init PROJECT_NAME --services auth' to add services")

aegis/core/template_generator.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
from .component_utils import extract_base_component_name, extract_engine_info
1212
from .components import COMPONENTS
13+
from .services import SERVICES
1314

1415

1516
class TemplateGenerator:
@@ -20,6 +21,7 @@ def __init__(
2021
project_name: str,
2122
selected_components: list[str],
2223
scheduler_backend: str = "memory",
24+
selected_services: list[str] | None = None,
2325
):
2426
"""
2527
Initialize template generator.
@@ -28,10 +30,12 @@ def __init__(
2830
project_name: Name of the project being generated
2931
selected_components: List of component names to include
3032
scheduler_backend: Scheduler backend: "memory", "sqlite", "postgres"
33+
selected_services: List of service names to include
3134
"""
3235
self.project_name = project_name
3336
self.project_slug = project_name.lower().replace(" ", "-").replace("_", "-")
3437
self.scheduler_backend = scheduler_backend
38+
self.selected_services = selected_services or []
3539

3640
# Always include core components
3741
all_components = ["backend", "frontend"] + selected_components
@@ -98,6 +102,8 @@ def get_template_context(self) -> dict[str, Any]:
98102
name in self.components for name in ["worker", "scheduler"]
99103
),
100104
"needs_redis": "redis" in self.components,
105+
# Service flags for template conditionals
106+
"include_auth": "yes" if "auth" in self.selected_services else "no",
101107
# Dependency lists for templates
102108
"selected_components": selected_only, # Original selection for context
103109
"docker_services": self._get_docker_services(),
@@ -127,11 +133,20 @@ def _get_pyproject_deps(self) -> list[str]:
127133
Sorted list of Python package dependencies
128134
"""
129135
deps = []
136+
# Collect component dependencies
130137
for component_name in self.components:
131138
if component_name in self.component_specs:
132139
spec = self.component_specs[component_name]
133140
if spec.pyproject_deps:
134141
deps.extend(spec.pyproject_deps)
142+
143+
# Collect service dependencies
144+
for service_name in self.selected_services:
145+
if service_name in SERVICES:
146+
service_spec = SERVICES[service_name]
147+
if service_spec.pyproject_deps:
148+
deps.extend(service_spec.pyproject_deps)
149+
135150
return sorted(set(deps)) # Sort and deduplicate
136151

137152
def get_template_files(self) -> list[str]:
@@ -142,12 +157,21 @@ def get_template_files(self) -> list[str]:
142157
List of template file paths
143158
"""
144159
files = []
160+
# Collect component template files
145161
for component_name in self.components:
146162
base_name = extract_base_component_name(component_name)
147163
if base_name in self.component_specs:
148164
spec = self.component_specs[base_name]
149165
if spec.template_files:
150166
files.extend(spec.template_files)
167+
168+
# Collect service template files
169+
for service_name in self.selected_services:
170+
if service_name in SERVICES:
171+
service_spec = SERVICES[service_name]
172+
if service_spec.template_files:
173+
files.extend(service_spec.template_files)
174+
151175
return list(dict.fromkeys(files)) # Preserve order, remove duplicates
152176

153177
def get_entrypoints(self) -> list[str]:

aegis/templates/cookiecutter-aegis-project/cookiecutter.json

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@
1717
"scheduler_backend": "memory",
1818
"scheduler_with_persistence": "no",
1919

20+
"_comment_services": "Service selection - these will be set by our CLI",
21+
"include_auth": "no",
22+
2023
"_comment_internal": "Internal variables for template logic",
2124
"_has_additional_components": "{% if cookiecutter.include_scheduler == 'yes' or cookiecutter.include_redis == 'yes' or cookiecutter.include_worker == 'yes' or cookiecutter.include_database == 'yes' or cookiecutter.include_cache == 'yes' %}yes{% else %}no{% endif %}",
2225

@@ -25,5 +28,8 @@
2528
"_redis_deps": "{% if cookiecutter.include_redis == 'yes' %}redis>=5.0.0{% endif %}",
2629
"_worker_deps": "{% if cookiecutter.include_worker == 'yes' %}arq>=0.25.0{% endif %}",
2730
"_database_deps": "{% if cookiecutter.include_database == 'yes' %}sqlmodel>=0.0.14,sqlalchemy>=2.0.0,aiosqlite>=0.19.0{% endif %}",
28-
"_cache_deps": "{% if cookiecutter.include_cache == 'yes' %}redis[hiredis]>=5.0.0{% endif %}"
31+
"_cache_deps": "{% if cookiecutter.include_cache == 'yes' %}redis[hiredis]>=5.0.0{% endif %}",
32+
33+
"_comment_service_dependencies": "Service-specific dependencies",
34+
"_auth_deps": "{% if cookiecutter.include_auth == 'yes' %}python-jose[cryptography]==3.3.0,passlib[bcrypt]==1.7.4,python-multipart==0.0.9{% endif %}"
2935
}

aegis/templates/cookiecutter-aegis-project/hooks/post_gen_project.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ def process_j2_templates():
4545
"include_worker": "{{ cookiecutter.include_worker }}",
4646
"include_database": "{{ cookiecutter.include_database }}",
4747
"include_cache": "{{ cookiecutter.include_cache }}",
48+
"include_auth": "{{ cookiecutter.include_auth }}",
4849
}
4950
}
5051

@@ -173,6 +174,18 @@ def main():
173174
# remove_file("app/services/cache_service.py")
174175
pass # Placeholder for cache component
175176

177+
# Remove services not selected
178+
if "{{ cookiecutter.include_auth }}" != "yes":
179+
# Remove auth service files
180+
remove_dir("app/components/backend/api/auth")
181+
remove_file("app/models/user.py")
182+
remove_dir("app/services/auth")
183+
remove_file("app/core/security.py")
184+
# Remove auth-related tests if they exist
185+
remove_file("tests/api/test_auth_endpoints.py")
186+
remove_file("tests/services/test_auth_service.py")
187+
remove_file("tests/models/test_user.py")
188+
176189
# Clean up empty docs/components directory if no components selected
177190
if (
178191
"{{ cookiecutter.include_scheduler }}" != "yes"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""Auth API endpoints."""
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
"""Authentication API routes."""
2+
3+
from app.core.db import get_db_session
4+
from app.core.security import create_access_token, verify_password
5+
from app.models.user import UserCreate, UserResponse
6+
from app.services.auth.auth_service import get_current_user_from_token
7+
from app.services.auth.user_service import UserService
8+
from fastapi import APIRouter, Depends, HTTPException, status
9+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
10+
from sqlmodel import Session
11+
12+
router = APIRouter(prefix="/auth", tags=["authentication"])
13+
14+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token")
15+
16+
17+
@router.post("/register", response_model=UserResponse)
18+
async def register(user_data: UserCreate, db: Session = Depends(get_db_session)):
19+
"""Register a new user."""
20+
user_service = UserService(db)
21+
22+
# Check if user already exists
23+
existing_user = user_service.get_user_by_email(user_data.email)
24+
if existing_user:
25+
raise HTTPException(
26+
status_code=status.HTTP_400_BAD_REQUEST, detail="Email already registered"
27+
)
28+
29+
# Create new user
30+
user = user_service.create_user(user_data)
31+
return UserResponse.model_validate(user)
32+
33+
34+
@router.post("/token")
35+
async def login(
36+
form_data: OAuth2PasswordRequestForm = Depends(),
37+
db: Session = Depends(get_db_session),
38+
):
39+
"""Login and get access token."""
40+
user_service = UserService(db)
41+
42+
# Get user by email (username field in OAuth2 form)
43+
user = user_service.get_user_by_email(form_data.username)
44+
45+
if not user or not verify_password(form_data.password, user.hashed_password):
46+
raise HTTPException(
47+
status_code=status.HTTP_401_UNAUTHORIZED,
48+
detail="Incorrect email or password",
49+
headers={"WWW-Authenticate": "Bearer"},
50+
)
51+
52+
# Create access token
53+
access_token = create_access_token(data={"sub": user.email})
54+
return {"access_token": access_token, "token_type": "bearer"}
55+
56+
57+
@router.get("/me", response_model=UserResponse)
58+
async def get_current_user(
59+
token: str = Depends(oauth2_scheme),
60+
db: Session = Depends(get_db_session),
61+
):
62+
"""Get current authenticated user."""
63+
user = await get_current_user_from_token(token, db)
64+
return UserResponse.model_validate(user)

aegis/templates/cookiecutter-aegis-project/{{cookiecutter.project_slug}}/app/core/config.py.j2

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,11 @@ class Settings(BaseSettings):
5555
# Flet frontend settings
5656
FLET_ASSETS_DIR: str = "assets" # Directory for Flet static assets (images, etc.)
5757

58+
# Authentication settings
59+
SECRET_KEY: str = "change-this-secret-key-in-production-use-env-variable"
60+
JWT_ALGORITHM: str = "HS256"
61+
ACCESS_TOKEN_EXPIRE_MINUTES: int = 30
62+
5863
{% if cookiecutter.include_redis == "yes" %}
5964
# Redis settings for arq background tasks
6065
REDIS_URL: str = "redis://redis:6379" # Docker service name by default

0 commit comments

Comments
 (0)