diff --git a/LICENSE b/LICENSE index e4bde0c..ff21bb4 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2024 fastappkit contributors +Copyright (c) 2025 fastappkit contributors Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 5834927..88c7840 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -19,8 +19,8 @@ apps = [ ### App Entry Formats -- `apps.` → Internal app (located in `./apps//`) -- `` → External app (pip-installed package, must be importable) +- `apps.` → Internal app (located in `./apps//`) +- `` → External app (pip-installed package, must be importable) ### Migration Order @@ -36,6 +36,7 @@ order = ["core", "auth", "blog"] Settings are defined in `core/config.py` using Pydantic's `BaseSettings`: ```python +from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): @@ -45,12 +46,19 @@ class Settings(BaseSettings): Loads from .env file automatically. """ - DATABASE_URL: str = "sqlite:///./app.db" - DEBUG: bool = False + database_url: str = Field( + default="sqlite:///./app.db", + alias="DATABASE_URL" + ) + debug: bool = Field( + default=False, + alias="DEBUG" + ) model_config = SettingsConfigDict( env_file=".env", - env_file_encoding="utf-8" + env_file_encoding="utf-8", + populate_by_name=True ) ``` @@ -59,30 +67,41 @@ class Settings(BaseSettings): The scaffolded code includes minimal settings. You can extend it by adding more fields: ```python +from pydantic import Field + class Settings(BaseSettings): - DATABASE_URL: str = "sqlite:///./app.db" - DEBUG: bool = False + database_url: str = Field( + default="sqlite:///./app.db", + alias="DATABASE_URL" + ) + debug: bool = Field( + default=False, + alias="DEBUG" + ) # Add your custom settings here - SECRET_KEY: str = "change-me-in-production" - HOST: str = "127.0.0.1" - PORT: int = 8000 + secret_key: str = Field( + default="change-me-in-production", + alias="SECRET_KEY" + ) + host: str = Field(default="127.0.0.1", alias="HOST") + port: int = Field(default=8000, alias="PORT") model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", - case_sensitive=True, + populate_by_name=True, extra="ignore", # Ignore extra fields from .env ) ``` ### Customization Options -- **Add Custom Settings:** Add fields to `Settings` class -- **Validation:** Use Pydantic validators: `@field_validator('DATABASE_URL')` -- **Default Values:** Set defaults in class definition -- **Nested Settings:** Use Pydantic models for nested configuration -- **Environment-Specific:** Override in `.env` or via environment variables +- **Add Custom Settings:** Add fields to `Settings` class with `Field()` instances +- **Validation:** Use Pydantic validators: `@field_validator('database_url')` +- **Default Values:** Set defaults in `Field()` instances +- **Nested Settings:** Use Pydantic models for nested configuration +- **Environment-Specific:** Override in `.env` or via environment variables (uppercase names are supported via aliases) ### Accessing Settings @@ -92,7 +111,7 @@ class Settings(BaseSettings): from fastappkit.conf import get_settings settings = get_settings() -db_url = settings.DATABASE_URL +db_url = settings.database_url ``` **FastAPI dependency injection:** @@ -103,7 +122,7 @@ from fastapi import Depends from core.config import Settings def handler(settings: Settings = Depends(get_settings)): - return settings.DEBUG + return settings.debug ``` ### Environment Variables @@ -198,28 +217,28 @@ route_prefix = "/blog" ``` !!! important "Manifest Requirements" - The manifest file is `fastappkit.toml`, not `pyproject.toml`. It must be located in the package directory (where `__init__.py` is). This ensures it's included when the package is published to PyPI. No fallback - `fastappkit.toml` is required for external apps. +The manifest file is `fastappkit.toml`, not `pyproject.toml`. It must be located in the package directory (where `__init__.py` is). This ensures it's included when the package is published to PyPI. No fallback - `fastappkit.toml` is required for external apps. See the [Manifest Reference](../reference/manifest-reference.md) for complete details. ## Dependency Versions !!! warning "Default Dependency Versions" - When creating a new project or external app, dependency versions in `pyproject.toml` are set to `*` (any version) by default. This provides maximum flexibility but may lead to compatibility issues. +When creating a new project or external app, dependency versions in `pyproject.toml` are set to `*` (any version) by default. This provides maximum flexibility but may lead to compatibility issues. ### Recommendations **For Production Projects:** -- Update dependency versions to specific ranges (e.g., `>=0.120.0,<0.130`) -- Pin exact versions for critical dependencies -- Test thoroughly after updating versions +- Update dependency versions to specific ranges (e.g., `>=0.120.0,<0.130`) +- Pin exact versions for critical dependencies +- Test thoroughly after updating versions **For External Apps:** -- Match dependency versions with the core project for compatibility -- Use compatible version ranges that work with the core project's versions -- Document minimum required versions in your app's README +- Match dependency versions with the core project for compatibility +- Use compatible version ranges that work with the core project's versions +- Document minimum required versions in your app's README **Example:** @@ -232,9 +251,9 @@ alembic = ">=1.17.2,<1.18" ``` !!! note "CLI Warning" - The CLI will display a warning message when creating projects or external apps, reminding you to update dependency versions according to your needs. +The CLI will display a warning message when creating projects or external apps, reminding you to update dependency versions according to your needs. ## Learn More -- [Configuration Reference](../reference/configuration-reference.md) - Complete configuration options -- [Manifest Reference](../reference/manifest-reference.md) - External app manifest schema +- [Configuration Reference](../reference/configuration-reference.md) - Complete configuration options +- [Manifest Reference](../reference/manifest-reference.md) - External app manifest schema diff --git a/docs/guides/creating-projects.md b/docs/guides/creating-projects.md index d7fddb1..045992e 100644 --- a/docs/guides/creating-projects.md +++ b/docs/guides/creating-projects.md @@ -50,13 +50,14 @@ myproject/ Start the development server: ```bash -fastappkit core dev [--host ] [--port ] [--reload] [--verbose] [--debug] [--quiet] +fastappkit core dev [--host ] [--port ] [--reload] [--verbose] [--debug] [--quiet] [] ``` ### Options - `--host, -h`: Host to bind to (default: `127.0.0.1`) - `--port, -p`: Port to bind to (default: `8000`) +- Additional uvicorn options: All other arguments are forwarded to uvicorn (e.g., `--workers`, `--log-level`, `--access-log`) - `--reload`: Enable auto-reload on code changes - `--verbose, -v`: Enable verbose output (overrides global setting) - `--debug`: Enable debug output (overrides global setting, includes stack traces) diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index 7676f0e..c6a5ff7 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -15,7 +15,7 @@ DEBUG=false ``` !!! warning "Security" - Never commit `.env` files with sensitive data. Use environment variables or secure secret management systems in production. +Never commit `.env` files with sensitive data. Use environment variables or secure secret management systems in production. ### Dependency Versions @@ -38,7 +38,7 @@ fastappkit migrate all ``` !!! tip "Migration Strategy" - Run migrations in a separate step before deploying application code. This ensures the database schema is ready before the new code runs. +Run migrations in a separate step before deploying application code. This ensures the database schema is ready before the new code runs. ## Deployment Options @@ -106,7 +106,7 @@ gunicorn main:app -w 4 -k uvicorn.workers.UvicornWorker --bind 0.0.0.0:8000 ``` !!! note "Development Server" - The `fastappkit core dev` command is intended for development only. Use a production ASGI server for production deployments. +The `fastappkit core dev` command is intended for development only. Use a production ASGI server for production deployments. ## Monitoring and Logging @@ -115,18 +115,22 @@ Configure proper logging for production: ```python # core/config.py import logging +from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): - DATABASE_URL: str - DEBUG: bool = False - LOG_LEVEL: str = "INFO" + database_url: str = Field(alias="DATABASE_URL") + debug: bool = Field(default=False, alias="DEBUG") + log_level: str = Field(default="INFO", alias="LOG_LEVEL") - model_config = SettingsConfigDict(env_file=".env") + model_config = SettingsConfigDict( + env_file=".env", + populate_by_name=True + ) # Configure logging logging.basicConfig( - level=getattr(logging, settings.LOG_LEVEL), + level=getattr(logging, settings.log_level), format='%(asctime)s - %(name)s - %(levelname)s - %(message)s' ) ``` @@ -148,5 +152,5 @@ def health_check(): ## Learn More -- [Configuration Guide](configuration.md) - Production configuration -- [Best Practices](../advanced/best-practices.md) - Production best practices +- [Configuration Guide](configuration.md) - Production configuration +- [Best Practices](../advanced/best-practices.md) - Production best practices diff --git a/docs/reference/api-reference.md b/docs/reference/api-reference.md index b87e317..de37dc1 100644 --- a/docs/reference/api-reference.md +++ b/docs/reference/api-reference.md @@ -23,10 +23,10 @@ app = kit.create_app() Create and configure FastAPI application. -- Loads all apps from `fastappkit.toml` -- Validates app manifests -- Mounts routers with automatic prefixes -- Returns configured FastAPI application +- Loads all apps from `fastappkit.toml` +- Validates app manifests +- Mounts routers with automatic prefixes +- Returns configured FastAPI application ## Settings API @@ -38,7 +38,7 @@ Get the current settings instance. from fastappkit.conf import get_settings settings = get_settings() -db_url = settings.DATABASE_URL +db_url = settings.database_url ``` ### set_settings() @@ -198,5 +198,5 @@ print(sql) ## Learn More -- [Architecture](../advanced/architecture.md) - System architecture -- [Extending fastappkit](../advanced/extending-fastappkit.md) - Extension guide +- [Architecture](../advanced/architecture.md) - System architecture +- [Extending fastappkit](../advanced/extending-fastappkit.md) - Extension guide diff --git a/docs/reference/cli-reference.md b/docs/reference/cli-reference.md index f57adf1..1d9f57c 100644 --- a/docs/reference/cli-reference.md +++ b/docs/reference/cli-reference.md @@ -6,10 +6,10 @@ Complete reference for all fastappkit CLI commands. All commands support these global options: -- `--verbose, -v`: Enable verbose output -- `--debug`: Enable debug output (includes stack traces) -- `--quiet, -q`: Suppress output -- `--version, -V`: Show version and exit +- `--verbose, -v`: Enable verbose output +- `--debug`: Enable debug output (includes stack traces) +- `--quiet, -q`: Suppress output +- `--version, -V`: Show version and exit ### Show Version @@ -35,12 +35,12 @@ fastappkit core new [--project-root ] [--description ] **Arguments:** -- ``: Project name (required) +- ``: Project name (required) **Options:** -- `--project-root `: Directory to create project in (default: current working directory) -- `--description `: Project description +- `--project-root `: Directory to create project in (default: current working directory) +- `--description `: Project description **Examples:** @@ -57,19 +57,20 @@ Run the development server. **Syntax:** ```bash -fastappkit core dev [--host ] [--port ] [--reload] [--verbose] [--debug] [--quiet] +fastappkit core dev [--host ] [--port ] [--reload] [--verbose] [--debug] [--quiet] [] ``` **Options:** -- `--host, -h `: Host to bind to (default: `127.0.0.1`) -- `--port, -p `: Port to bind to (default: `8000`) -- `--reload`: Enable auto-reload on code changes -- `--verbose, -v`: Enable verbose output (overrides global setting) -- `--debug`: Enable debug output (overrides global setting, includes stack traces) -- `--quiet, -q`: Suppress output (overrides global setting) +- `--host, -h `: Host to bind to (default: `127.0.0.1`) +- `--port, -p `: Port to bind to (default: `8000`) +- `--reload`: Enable auto-reload on code changes +- `--verbose, -v`: Enable verbose output (overrides global setting) +- `--debug`: Enable debug output (overrides global setting, includes stack traces) +- `--quiet, -q`: Suppress output (overrides global setting) +- ``: Additional options are forwarded to uvicorn (e.g., `--workers`, `--log-level`, `--access-log`) -**Note:** This command must be run from the project root directory (where `fastappkit.toml` is located). +**Note:** This command must be run from the project root directory (where `fastappkit.toml` is located). All additional arguments are forwarded to uvicorn, allowing you to use any uvicorn option. **Examples:** @@ -77,6 +78,9 @@ fastappkit core dev [--host ] [--port ] [--reload] [--verbose] [--de fastappkit core dev fastappkit core dev --host 0.0.0.0 --port 8080 fastappkit core dev --reload +fastappkit core dev --workers 4 +fastappkit core dev --log-level debug +fastappkit core dev --access-log ``` ## App Commands @@ -93,11 +97,11 @@ fastappkit app new [--as-package] **Arguments:** -- ``: App name (required) +- ``: App name (required) **Options:** -- `--as-package`: Create as external package (required for external apps) +- `--as-package`: Create as external package (required for external apps) **Note:** This command must be run from the project root directory (where `fastappkit.toml` is located). @@ -120,9 +124,9 @@ fastappkit app list [--verbose] [--debug] [--quiet] **Options:** -- `--verbose, -v`: Show detailed information (import path, migrations path, etc.) -- `--debug`: Show debug information -- `--quiet, -q`: Suppress output +- `--verbose, -v`: Show detailed information (import path, migrations path, etc.) +- `--debug`: Show debug information +- `--quiet, -q`: Suppress output **Note:** This command must be run from the project root directory. @@ -145,11 +149,11 @@ fastappkit app validate [--json] **Arguments:** -- ``: App name (required) +- ``: App name (required) **Options:** -- `--json`: Output results as JSON (CI-friendly) +- `--json`: Output results as JSON (CI-friendly) **Note:** This command must be run from the project root directory. @@ -174,7 +178,7 @@ fastappkit migrate core -m **Options:** -- `-m, --message `: Migration message (required) +- `-m, --message `: Migration message (required) **Note:** This command must be run from the project root directory. @@ -196,33 +200,33 @@ fastappkit migrate app [options] **Arguments:** -- ``: App name (required) -- ``: Migration action (required) +- ``: App name (required) +- ``: Migration action (required) **Actions:** -- `makemigrations`: Generate new migration (internal apps only) -- `upgrade`: Apply migrations (external apps only) -- `downgrade`: Revert migrations (external apps only) -- `preview`: Show SQL without executing (external apps only) +- `makemigrations`: Generate new migration (internal apps only) +- `upgrade`: Apply migrations (external apps only) +- `downgrade`: Revert migrations (external apps only) +- `preview`: Show SQL without executing (external apps only) **Options:** -- `-m, --message `: Migration message (required for `makemigrations`) -- `--revision, -r `: Specific revision (default: `head` for upgrade/preview) +- `-m, --message `: Migration message (required for `makemigrations`) +- `--revision, -r `: Specific revision (default: `head` for upgrade/preview) **Note:** This command must be run from the project root directory. **Limitations:** -- Internal apps can only use `makemigrations` action. For preview/upgrade/downgrade, use unified commands (`fastappkit migrate preview/upgrade/downgrade`). -- External apps cannot use `makemigrations` from the core project. Migrations must be created in the external app's own directory using `alembic` directly. +- Internal apps can only use `makemigrations` action. For preview/upgrade/downgrade, use unified commands (`fastappkit migrate preview/upgrade/downgrade`). +- External apps cannot use `makemigrations` from the core project. Migrations must be created in the external app's own directory using `alembic` directly. **Error Scenarios:** -- If external app has no migration files, the command will fail with instructions on how to create migrations independently. -- If revision is not found, the command will fail with helpful error messages. -- If app is not found in registry, the command will fail with app name error. +- If external app has no migration files, the command will fail with instructions on how to create migrations independently. +- If revision is not found, the command will fail with helpful error messages. +- If app is not found in registry, the command will fail with app name error. **Examples:** @@ -255,7 +259,7 @@ fastappkit migrate preview [--revision ] **Options:** -- `--revision, -r `: Specific revision (default: `head`) +- `--revision, -r `: Specific revision (default: `head`) **Note:** This command must be run from the project root directory. @@ -278,7 +282,7 @@ fastappkit migrate upgrade [--revision ] **Options:** -- `--revision, -r `: Specific revision (default: `head`) +- `--revision, -r `: Specific revision (default: `head`) **Note:** This command must be run from the project root directory. @@ -301,7 +305,7 @@ fastappkit migrate downgrade **Arguments:** -- ``: Revision to downgrade to (required) +- ``: Revision to downgrade to (required) **Note:** This command must be run from the project root directory. @@ -337,22 +341,22 @@ fastappkit migrate all ## Command Reference Table -| Command | Description | Options | -|---------|-------------|---------| -| `fastappkit core new ` | Create new project | `--project-root`, `--description` | -| `fastappkit core dev` | Run development server | `--host`, `--port`, `--reload`, `--verbose`, `--debug`, `--quiet` | -| `fastappkit app new ` | Create internal app | `--as-package` | -| `fastappkit app list` | List all apps | `--verbose`, `--debug`, `--quiet` | -| `fastappkit app validate ` | Validate app | `--json` | -| `fastappkit migrate core` | Core migrations | `-m, --message` | -| `fastappkit migrate app ` | App migrations | `makemigrations`, `upgrade`, `downgrade`, `preview` | -| `fastappkit migrate preview` | Preview SQL | `--revision` | -| `fastappkit migrate upgrade` | Upgrade migrations | `--revision` | -| `fastappkit migrate downgrade ` | Downgrade migrations | (revision required) | -| `fastappkit migrate all` | Apply all migrations | (no options) | +| Command | Description | Options | +| ------------------------------------ | ---------------------- | ------------------------------------------------------------------------------------------- | +| `fastappkit core new ` | Create new project | `--project-root`, `--description` | +| `fastappkit core dev` | Run development server | `--host`, `--port`, `--reload`, `--verbose`, `--debug`, `--quiet`, plus any uvicorn options | +| `fastappkit app new ` | Create internal app | `--as-package` | +| `fastappkit app list` | List all apps | `--verbose`, `--debug`, `--quiet` | +| `fastappkit app validate ` | Validate app | `--json` | +| `fastappkit migrate core` | Core migrations | `-m, --message` | +| `fastappkit migrate app ` | App migrations | `makemigrations`, `upgrade`, `downgrade`, `preview` | +| `fastappkit migrate preview` | Preview SQL | `--revision` | +| `fastappkit migrate upgrade` | Upgrade migrations | `--revision` | +| `fastappkit migrate downgrade ` | Downgrade migrations | (revision required) | +| `fastappkit migrate all` | Apply all migrations | (no options) | ## Learn More -- [Creating Projects](../guides/creating-projects.md) - Project creation guide -- [Creating Apps](../guides/creating-apps.md) - App creation guide -- [Migrations](../guides/migrations.md) - Migration workflows +- [Creating Projects](../guides/creating-projects.md) - Project creation guide +- [Creating Apps](../guides/creating-apps.md) - App creation guide +- [Migrations](../guides/migrations.md) - Migration workflows diff --git a/docs/reference/configuration-reference.md b/docs/reference/configuration-reference.md index c03d411..60d5780 100644 --- a/docs/reference/configuration-reference.md +++ b/docs/reference/configuration-reference.md @@ -19,8 +19,8 @@ apps = [ ### App Entry Formats -- `apps.` → Internal app (located in `./apps//`) -- `` → External app (pip-installed package, must be importable) +- `apps.` → Internal app (located in `./apps//`) +- `` → External app (pip-installed package, must be importable) ### Migration Order @@ -40,29 +40,48 @@ Settings are defined in `core/config.py` using Pydantic's `BaseSettings`. ### Basic Settings ```python +from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): - DATABASE_URL: str = "sqlite:///./app.db" - DEBUG: bool = False + database_url: str = Field( + default="sqlite:///./app.db", + alias="DATABASE_URL" + ) + debug: bool = Field( + default=False, + alias="DEBUG" + ) model_config = SettingsConfigDict( env_file=".env", - env_file_encoding="utf-8" + env_file_encoding="utf-8", + populate_by_name=True ) ``` ### Extended Settings ```python +from pydantic import Field + class Settings(BaseSettings): - DATABASE_URL: str = "sqlite:///./app.db" - DEBUG: bool = False + database_url: str = Field( + default="sqlite:///./app.db", + alias="DATABASE_URL" + ) + debug: bool = Field( + default=False, + alias="DEBUG" + ) # Custom settings - SECRET_KEY: str = "change-me-in-production" - HOST: str = "127.0.0.1" - PORT: int = 8000 + secret_key: str = Field( + default="change-me-in-production", + alias="SECRET_KEY" + ) + host: str = Field(default="127.0.0.1", alias="HOST") + port: int = Field(default=8000, alias="PORT") # Nested settings class DatabaseConfig(BaseSettings): @@ -75,7 +94,7 @@ class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", - case_sensitive=True, + populate_by_name=True, extra="ignore", ) ``` @@ -84,10 +103,10 @@ class Settings(BaseSettings): **BaseSettings Configuration:** -- `env_file`: Path to `.env` file (default: `.env`) -- `env_file_encoding`: Encoding for `.env` file (default: `utf-8`) -- `case_sensitive`: Whether environment variable names are case-sensitive (default: `False`) -- `extra`: How to handle extra fields (`"ignore"`, `"forbid"`, `"allow"`) +- `env_file`: Path to `.env` file (default: `.env`) +- `env_file_encoding`: Encoding for `.env` file (default: `utf-8`) +- `case_sensitive`: Whether environment variable names are case-sensitive (default: `False`) +- `extra`: How to handle extra fields (`"ignore"`, `"forbid"`, `"allow"`) ### Environment Variables @@ -112,5 +131,5 @@ See the [Manifest Reference](manifest-reference.md) for complete details. ## Learn More -- [Configuration Guide](../guides/configuration.md) - Configuration guide -- [Manifest Reference](manifest-reference.md) - External app manifest schema +- [Configuration Guide](../guides/configuration.md) - Configuration guide +- [Manifest Reference](manifest-reference.md) - External app manifest schema diff --git a/fastappkit/cli/core.py b/fastappkit/cli/core.py index 2f0400d..4e0c8f2 100644 --- a/fastappkit/cli/core.py +++ b/fastappkit/cli/core.py @@ -8,6 +8,7 @@ import sys import traceback from pathlib import Path +from typing import Any import typer import uvicorn @@ -122,8 +123,9 @@ def new( raise typer.Exit(1) -@app.command() +@app.command(context_settings={"allow_extra_args": True, "ignore_unknown_options": True}) def dev( + ctx: typer.Context, host: str = typer.Option("127.0.0.1", "--host", "-h", help="Host to bind to"), port: int = typer.Option(8000, "--port", "-p", help="Port to bind to"), reload: bool = typer.Option(False, "--reload", help="Enable auto-reload"), @@ -131,7 +133,14 @@ def dev( debug: bool = typer.Option(False, "--debug", help="Enable debug output"), quiet: bool = typer.Option(False, "--quiet", "-q", help="Suppress output"), ) -> None: - """Run development server. Must be run from project root.""" + """Run development server. Must be run from project root. + + All additional arguments are forwarded to uvicorn. + Examples: + fastappkit core dev --workers 4 + fastappkit core dev --log-level debug + fastappkit core dev --access-log + """ # Override output level if flags are provided at command level if quiet or verbose or debug: if quiet: @@ -152,9 +161,23 @@ def dev( # This will execute core.app which initializes FastAppKit with Settings ensure_settings_loaded(project_root) + # Parse remaining arguments and convert to uvicorn kwargs + uvicorn_kwargs = _parse_uvicorn_args(ctx.args, host=host, port=port, reload=reload) + + # Determine log level based on output level + if output.level.value >= 3: # debug + uvicorn_kwargs.setdefault("log_level", "debug") + elif output.level.value >= 2: # verbose + uvicorn_kwargs.setdefault("log_level", "info") + else: + uvicorn_kwargs.setdefault("log_level", "warning") + # Run uvicorn - output.success(f"Starting development server on http://{host}:{port}") - if reload: + display_host = uvicorn_kwargs.get("host", host) + display_port = uvicorn_kwargs.get("port", port) + output.success(f"Starting development server on http://{display_host}:{display_port}") + + if uvicorn_kwargs.get("reload"): output.info("Auto-reload enabled") # For reload to work, uvicorn needs an import string, not an app object # Ensure project root is on sys.path so Python can import main module @@ -162,27 +185,15 @@ def dev( sys.path.insert(0, str(project_root)) # Use import string for reload (main:app imports core.app which sets up app) - uvicorn.run( - "main:app", # Import string: module "main", variable "app" - host=host, - port=port, - reload=reload, - log_level="info" if output.level.value >= 2 else "warning", - ) + uvicorn.run("main:app", **uvicorn_kwargs) else: # Without reload, import main:app to get the app object if str(project_root) not in sys.path: sys.path.insert(0, str(project_root)) - from main import app as fastapi_app # type: ignore[import-not-found] + from main import app as fastapi_app - uvicorn.run( - fastapi_app, - host=host, - port=port, - reload=reload, - log_level="info" if output.level.value >= 2 else "warning", - ) + uvicorn.run(fastapi_app, **uvicorn_kwargs) except ConfigError as e: output.error(f"Configuration error: {e}") raise typer.Exit(1) @@ -191,3 +202,76 @@ def dev( if output.level.value >= 3: # debug level traceback.print_exc() raise typer.Exit(1) + + +def _parse_uvicorn_args(args: list[str], host: str, port: int, reload: bool) -> dict[str, Any]: + """ + Parse command-line arguments and convert them to uvicorn kwargs. + + Args: + args: Remaining command-line arguments + host: Default host value + port: Default port value + reload: Default reload value + + Returns: + Dictionary of uvicorn kwargs + """ + kwargs: dict[str, object] = { + "host": host, + "port": port, + "reload": reload, + } + + i = 0 + while i < len(args): + arg = args[i] + + # Handle --flag (boolean flags) + if arg.startswith("--") and "=" not in arg: + flag_name = arg[2:].replace("-", "_") + + # Check if next arg is a value (not another flag) + if i + 1 < len(args) and not args[i + 1].startswith("-"): + value = args[i + 1] + # Try to convert to appropriate type + if value.lower() in ("true", "1", "yes", "on"): + kwargs[flag_name] = True + elif value.lower() in ("false", "0", "no", "off"): + kwargs[flag_name] = False + elif value.isdigit(): + kwargs[flag_name] = int(value) + else: + kwargs[flag_name] = value + i += 2 + else: + # Boolean flag (presence means True) + kwargs[flag_name] = True + i += 1 + + # Handle --flag=value + elif arg.startswith("--") and "=" in arg: + flag_part, value_part = arg[2:].split("=", 1) + flag_name = flag_part.replace("-", "_") + + # Try to convert to appropriate type + if value_part.lower() in ("true", "1", "yes", "on"): + kwargs[flag_name] = True + elif value_part.lower() in ("false", "0", "no", "off"): + kwargs[flag_name] = False + elif value_part.isdigit(): + kwargs[flag_name] = int(value_part) + else: + kwargs[flag_name] = value_part + i += 1 + + # Handle -f (short flags) + elif arg.startswith("-") and not arg.startswith("--") and len(arg) > 1: + # Short flags are typically single character, skip for now + # (uvicorn mostly uses long-form flags) + i += 1 + else: + # Unknown argument, skip + i += 1 + + return kwargs diff --git a/fastappkit/cli/templates/app/internal/migrations/env.py.j2 b/fastappkit/cli/templates/app/internal/migrations/env.py.j2 index 751901d..0b22f68 100644 --- a/fastappkit/cli/templates/app/internal/migrations/env.py.j2 +++ b/fastappkit/cli/templates/app/internal/migrations/env.py.j2 @@ -27,7 +27,12 @@ settings = Settings() config = context.config # Set database URL from settings -config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) +# Strip async drivers (e.g., +asyncpg, +aiosqlite) since Alembic doesn't support async +database_url = settings.database_url +# Remove common async driver suffixes +for async_driver in ["+asyncpg", "+aiosqlite", "+asyncmy"]: + database_url = database_url.replace(async_driver, "") +config.set_main_option("sqlalchemy.url", database_url) # Interpret the config file for Python logging if config.config_file_name is not None: diff --git a/fastappkit/cli/templates/project/core/config.py.j2 b/fastappkit/cli/templates/project/core/config.py.j2 index c56a820..c68e7fb 100644 --- a/fastappkit/cli/templates/project/core/config.py.j2 +++ b/fastappkit/cli/templates/project/core/config.py.j2 @@ -4,6 +4,7 @@ Settings configuration for {{ project_name }}. Uses pydantic-settings to load from .env file. """ +from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict @@ -14,10 +15,17 @@ class Settings(BaseSettings): Loads from .env file automatically. """ - DATABASE_URL: str = "sqlite:///./{{ project_name }}.db" - DEBUG: bool = False + database_url: str = Field( + default="sqlite:///./{{ project_name }}.db", + alias="DATABASE_URL" + ) + debug: bool = Field( + default=False, + alias="DEBUG" + ) model_config = SettingsConfigDict( env_file=".env", - env_file_encoding="utf-8" + env_file_encoding="utf-8", + populate_by_name=True ) diff --git a/fastappkit/cli/templates/project/core/db/migrations/env.py.j2 b/fastappkit/cli/templates/project/core/db/migrations/env.py.j2 index def0623..0c10bd7 100644 --- a/fastappkit/cli/templates/project/core/db/migrations/env.py.j2 +++ b/fastappkit/cli/templates/project/core/db/migrations/env.py.j2 @@ -38,7 +38,12 @@ if target_metadata is None: target_metadata = MetaData() # Set database URL from settings -config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) +# Strip async drivers (e.g., +asyncpg, +aiosqlite) since Alembic doesn't support async +database_url = settings.database_url +# Remove common async driver suffixes +for async_driver in ["+asyncpg", "+aiosqlite", "+asyncmy"]: + database_url = database_url.replace(async_driver, "") +config.set_main_option("sqlalchemy.url", database_url) # Interpret the config file for Python logging if config.config_file_name is not None: diff --git a/fastappkit/conf/protocol.py b/fastappkit/conf/protocol.py index 664aacc..3a6bf62 100644 --- a/fastappkit/conf/protocol.py +++ b/fastappkit/conf/protocol.py @@ -18,7 +18,7 @@ class SettingsProtocol(Protocol): """ # Database configuration - DATABASE_URL: str + database_url: str # Application settings - DEBUG: bool + debug: bool diff --git a/fastappkit/core/kit.py b/fastappkit/core/kit.py index 9fbe99b..8627abd 100644 --- a/fastappkit/core/kit.py +++ b/fastappkit/core/kit.py @@ -50,7 +50,7 @@ def create_app(self) -> FastAPI: """ app = FastAPI( title="FastAppKit Application", - debug=self.settings.DEBUG, + debug=self.settings.debug, ) # Load apps diff --git a/fastappkit/migrations/context.py b/fastappkit/migrations/context.py index c85dc8d..f0fe9a4 100644 --- a/fastappkit/migrations/context.py +++ b/fastappkit/migrations/context.py @@ -120,7 +120,12 @@ def build_alembic_config( # External apps have their own isolated migration directories # Set database URL from settings (core project's database) + # Strip async drivers (e.g., +asyncpg, +aiosqlite) since Alembic doesn't support async settings = get_settings() - alembic_cfg.set_main_option("sqlalchemy.url", settings.DATABASE_URL) + database_url = settings.database_url + # Remove common async driver suffixes + for async_driver in ["+asyncpg", "+aiosqlite", "+asyncmy"]: + database_url = database_url.replace(async_driver, "") + alembic_cfg.set_main_option("sqlalchemy.url", database_url) return alembic_cfg diff --git a/fastappkit/migrations/runner.py b/fastappkit/migrations/runner.py index 774ad7e..02450fd 100644 --- a/fastappkit/migrations/runner.py +++ b/fastappkit/migrations/runner.py @@ -30,7 +30,7 @@ def __init__(self) -> None: def _get_engine(self) -> Engine: """Get SQLAlchemy engine from settings.""" - return create_engine(self.settings.DATABASE_URL) + return create_engine(self.settings.database_url) def upgrade( self, diff --git a/tests/conftest.py b/tests/conftest.py index 70e9f2b..dd8cd93 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -9,6 +9,7 @@ import pytest from fastapi.testclient import TestClient +from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict from fastappkit.conf import set_settings @@ -18,14 +19,14 @@ class TestSettings(BaseSettings): """Test settings class that implements SettingsProtocol.""" - DATABASE_URL: str = "sqlite:///:memory:" - DEBUG: bool = True + database_url: str = Field(default="sqlite:///:memory:") + debug: bool = Field(default=True) SECRET_KEY: str = "test-secret-key" HOST: str = "127.0.0.1" PORT: int = 8000 RELOAD: bool = False - model_config = SettingsConfigDict(env_file=".env", extra="ignore") + model_config = SettingsConfigDict(env_file=".env", extra="ignore", populate_by_name=True) @pytest.fixture @@ -71,8 +72,8 @@ def test_settings() -> TestSettings: TestSettings instance for testing """ return TestSettings( - DATABASE_URL="sqlite:///:memory:", - DEBUG=True, + database_url="sqlite:///:memory:", + debug=True, ) diff --git a/tests/integration/test_cli/test_migrate_commands.py b/tests/integration/test_cli/test_migrate_commands.py index efbd545..359e230 100644 --- a/tests/integration/test_cli/test_migrate_commands.py +++ b/tests/integration/test_cli/test_migrate_commands.py @@ -65,7 +65,7 @@ def downgrade(): config = context.config settings = get_settings() -config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) +config.set_main_option("sqlalchemy.url", settings.database_url) config.set_main_option("version_table", "alembic_version") target_metadata = MetaData() @@ -85,7 +85,7 @@ def run_migrations_offline(): def run_migrations_online(): """Run migrations in 'online' mode.""" connectable = engine_from_config( - {"sqlalchemy.url": settings.DATABASE_URL}, + {"sqlalchemy.url": settings.database_url}, prefix="sqlalchemy.", poolclass=None, ) diff --git a/tests/unit/test_core/test_kit.py b/tests/unit/test_core/test_kit.py index 9c15392..512e3f3 100644 --- a/tests/unit/test_core/test_kit.py +++ b/tests/unit/test_core/test_kit.py @@ -53,7 +53,7 @@ def test_create_app_creates_fastapi_instance( assert isinstance(app, FastAPI) assert app.title == "FastAppKit Application" - assert app.debug == test_settings.DEBUG + assert app.debug == test_settings.debug finally: os.chdir(original_cwd) diff --git a/tests/unit/test_migrations/test_runner.py b/tests/unit/test_migrations/test_runner.py index b7e0cf6..9e689ad 100644 --- a/tests/unit/test_migrations/test_runner.py +++ b/tests/unit/test_migrations/test_runner.py @@ -27,7 +27,7 @@ def test_upgrade_requires_valid_migration_path( self, temp_project: Path, test_settings: TestSettings ) -> None: """upgrade() requires valid migration path.""" - test_settings.DATABASE_URL = "sqlite:///:memory:" + test_settings.database_url = "sqlite:///:memory:" set_settings(test_settings) # Create app metadata without migrations_path @@ -47,7 +47,7 @@ def test_get_current_revision_handles_missing_migrations_path( self, temp_project: Path, test_settings: TestSettings ) -> None: """get_current_revision() handles missing migrations path gracefully.""" - test_settings.DATABASE_URL = "sqlite:///:memory:" + test_settings.database_url = "sqlite:///:memory:" set_settings(test_settings) # Create app metadata with invalid migrations_path @@ -68,7 +68,7 @@ def test_upgrade_raises_error_on_invalid_migration( self, temp_project: Path, test_settings: TestSettings ) -> None: """upgrade() raises MigrationError on invalid migration.""" - test_settings.DATABASE_URL = "sqlite:///:memory:" + test_settings.database_url = "sqlite:///:memory:" set_settings(test_settings) migrations_path = temp_project / "migrations" @@ -96,14 +96,14 @@ def downgrade(): config = context.config settings = get_settings() -config.set_main_option("sqlalchemy.url", settings.DATABASE_URL) +config.set_main_option("sqlalchemy.url", settings.database_url) config.set_main_option("version_table", "alembic_version") target_metadata = None def run_migrations_online(): connectable = engine_from_config( - {"sqlalchemy.url": settings.DATABASE_URL}, + {"sqlalchemy.url": settings.database_url}, prefix="sqlalchemy.", poolclass=None, )