diff --git a/docs/guides/configuration.md b/docs/guides/configuration.md index 88c7840..b24472f 100644 --- a/docs/guides/configuration.md +++ b/docs/guides/configuration.md @@ -244,7 +244,7 @@ When creating a new project or external app, dependency versions in `pyproject.t ```toml [tool.poetry.dependencies] -python = "^3.11" +python = ">=3.11,<4.0" fastapi = ">=0.120.0,<0.130" # Specific range instead of * sqlalchemy = ">=2.0,<3.0" alembic = ">=1.17.2,<1.18" diff --git a/docs/guides/deployment.md b/docs/guides/deployment.md index c6a5ff7..47b0571 100644 --- a/docs/guides/deployment.md +++ b/docs/guides/deployment.md @@ -23,7 +23,7 @@ Update dependency versions in `pyproject.toml` from `*` to specific ranges for p ```toml [tool.poetry.dependencies] -python = "^3.11" +python = ">=3.11,<4.0" fastapi = ">=0.120.0,<0.130" sqlalchemy = ">=2.0,<3.0" alembic = ">=1.17.2,<1.18" diff --git a/fastappkit/cli/app.py b/fastappkit/cli/app.py index 8c5bab1..e986f5c 100644 --- a/fastappkit/cli/app.py +++ b/fastappkit/cli/app.py @@ -126,8 +126,10 @@ def new( ("app/external/__init__.py.j2", f"{name}/{name}/__init__.py"), ("app/external/models.py.j2", f"{name}/{name}/models.py"), ("app/external/router.py.j2", f"{name}/{name}/router.py"), + ("app/external/config.py.j2", f"{name}/{name}/config.py"), ("app/external/pyproject.toml.j2", f"{name}/pyproject.toml"), ("app/external/README.md.j2", f"{name}/README.md"), + ("app/external/main.py.j2", f"{name}/main.py"), ] # Create fastappkit.toml INSIDE package directory (included in package when published) @@ -172,6 +174,11 @@ def new( context, ) output.verbose("Created .gitignore") + + # Create .env from env.example template + env_example_content = template_engine.render("app/external/env.example.j2", context) + (app_path / ".env").write_text(env_example_content, encoding="utf-8") + output.verbose("Created .env") else: # Internal app templates # Internal apps don't have their own migrations - they use core's migrations @@ -205,7 +212,9 @@ def new( output.info("\nNext steps:") output.info(f" cd {name}") output.info(" pip install -e . # Install the package") - output.info(f" # Add '{name}' to fastappkit.toml apps list") + output.info(" # Configure .env file (DATABASE_URL, DEBUG, etc.)") + output.info(" # Run independently: uvicorn main:app --reload") + output.info(" # Or add to fastappkit.toml apps list in a core project") output.info(f" fastappkit migrate app {name} makemigrations") output.warning( "\n⚠️ Note: Dependency versions in pyproject.toml are set to '*' (any version)." diff --git a/fastappkit/cli/templates/app/external/README.md.j2 b/fastappkit/cli/templates/app/external/README.md.j2 index 5ca0827..9830e88 100644 --- a/fastappkit/cli/templates/app/external/README.md.j2 +++ b/fastappkit/cli/templates/app/external/README.md.j2 @@ -8,9 +8,68 @@ External pluggable app for fastappkit. pip install -e . ``` -## Usage +## Configuration -Add to your `fastappkit.toml`: +This app uses a `.env` file for configuration. A `.env` file is created automatically when you create the app. + +**Configure your environment:** + +1. Edit the `.env` file in the app root directory: + ```bash + # Database Configuration + DATABASE_URL=sqlite:///./{{ app_name }}.db + + # Development Settings + DEBUG=false + ``` + +2. For production or different databases, update `DATABASE_URL`: + ```bash + DATABASE_URL=postgresql://user:password@localhost/{{ app_name }} + ``` + +The `.env` file is automatically loaded by `config.py` using `pydantic-settings`. + +## Independent Development + +This external app can be developed and tested independently without requiring a full fastappkit project. + +### Running the Server + +Start the development server: + +```bash +# From the app root directory +uvicorn main:app --reload +``` + +Or use Python directly: + +```bash +python main.py +``` + +The server will start on `http://127.0.0.1:8000` with the app's routes available at `/{{ app_name }}/`. + +### Project Structure + +``` +{{ app_name }}/ +├── {{ app_name }}/ # Package directory +│ ├── __init__.py # register() function +│ ├── models.py # SQLAlchemy models +│ ├── router.py # FastAPI routes +│ ├── config.py # Settings (loads from .env) +│ └── migrations/ # Alembic migrations +├── main.py # Entry point for independent development +├── alembic.ini # Alembic configuration +├── .env # Environment variables (gitignored) +└── pyproject.toml # Package metadata +``` + +## Usage in a Core Project + +To use this app in a fastappkit project, add it to your `fastappkit.toml`: ```toml [tool.fastappkit] @@ -53,7 +112,9 @@ alembic downgrade -1 alembic upgrade head --sql ``` -**Database URL**: Set `DATABASE_URL` environment variable, or edit `alembic.ini` to configure your database. +**Database URL**: The `DATABASE_URL` is read from the `.env` file automatically. You can also: +- Set `DATABASE_URL` environment variable (takes precedence) +- Edit `alembic.ini` sqlalchemy.url (fallback) ### When Using the External App in a Core Project diff --git a/fastappkit/cli/templates/app/external/alembic.ini.j2 b/fastappkit/cli/templates/app/external/alembic.ini.j2 index dd95c38..e444566 100644 --- a/fastappkit/cli/templates/app/external/alembic.ini.j2 +++ b/fastappkit/cli/templates/app/external/alembic.ini.j2 @@ -59,9 +59,18 @@ version_path_separator = os # Use os.pathsep. Default configuration used for ne # are written from script.py.mako # output_encoding = utf-8 -# Database URL - can be overridden via DATABASE_URL environment variable +# Database URL configuration +# +# This can be configured in three ways (in order of precedence): +# 1. DATABASE_URL environment variable (from .env file when running independently) +# 2. This sqlalchemy.url setting (fallback) +# 3. Set programmatically in migrations/env.py +# +# For independent development, create a .env file with: +# DATABASE_URL=sqlite:///./{{ app_name }}.db +# +# The migrations/env.py will automatically read DATABASE_URL from the environment. # Default uses SQLite for development -# Edit this line or set DATABASE_URL environment variable sqlalchemy.url = sqlite:///./{{ app_name }}.db diff --git a/fastappkit/cli/templates/app/external/config.py.j2 b/fastappkit/cli/templates/app/external/config.py.j2 new file mode 100644 index 0000000..e7c0302 --- /dev/null +++ b/fastappkit/cli/templates/app/external/config.py.j2 @@ -0,0 +1,31 @@ +""" +Settings configuration for {{ app_name }}. + +Uses pydantic-settings to load from .env file. +""" + +from pydantic import Field +from pydantic_settings import BaseSettings, SettingsConfigDict + + +class Settings(BaseSettings): + """ + Application settings. + + Loads from .env file automatically. + """ + + database_url: str = Field( + default="sqlite:///./{{ app_name }}.db", + alias="DATABASE_URL" + ) + debug: bool = Field( + default=False, + alias="DEBUG" + ) + + model_config = SettingsConfigDict( + env_file=".env", + env_file_encoding="utf-8", + populate_by_name=True + ) diff --git a/fastappkit/cli/templates/app/external/env.example.j2 b/fastappkit/cli/templates/app/external/env.example.j2 new file mode 100644 index 0000000..aef7687 --- /dev/null +++ b/fastappkit/cli/templates/app/external/env.example.j2 @@ -0,0 +1,5 @@ +# Database Configuration +DATABASE_URL=sqlite:///./{{ app_name }}.db + +# Development Settings +DEBUG=false diff --git a/fastappkit/cli/templates/app/external/main.py.j2 b/fastappkit/cli/templates/app/external/main.py.j2 new file mode 100644 index 0000000..065eded --- /dev/null +++ b/fastappkit/cli/templates/app/external/main.py.j2 @@ -0,0 +1,44 @@ +""" +Main entry point for {{ app_name }}. + +Run with: uvicorn main:app --reload + +This allows you to develop and test the external app independently +without requiring a full fastappkit project. +""" + +import sys +from pathlib import Path + +# Add current directory to sys.path so we can import the package +# This allows running without 'pip install -e .' (though that's recommended) +_current_dir = Path(__file__).parent +if str(_current_dir) not in sys.path: + sys.path.insert(0, str(_current_dir)) + +from fastapi import FastAPI + +from {{ app_name }}.config import Settings +from {{ app_name }} import register + +# Load settings from .env +settings = Settings() + +# Create FastAPI app +app = FastAPI( + title="{{ app_name }}", + description="External pluggable app for fastappkit", + debug=settings.debug, +) + +# Register this app's router +router = register(app) +if router is not None: + # If register() returns a router, mount it with default prefix + app.include_router(router, prefix="/{{ app_name }}", tags=["{{ app_name }}"]) + + +if __name__ == "__main__": + import uvicorn + + uvicorn.run(app, host="127.0.0.1", port=8000, reload=True) diff --git a/fastappkit/cli/templates/app/external/migrations/env.py.j2 b/fastappkit/cli/templates/app/external/migrations/env.py.j2 index 0413a46..727ef6b 100644 --- a/fastappkit/cli/templates/app/external/migrations/env.py.j2 +++ b/fastappkit/cli/templates/app/external/migrations/env.py.j2 @@ -31,12 +31,17 @@ target_metadata = Base.metadata config = context.config -# Get database URL from config (set by fastappkit when running from core project) -# or from environment variable (when running Alembic directly from external app) -# or from alembic.ini (fallback for direct Alembic usage) +# Get database URL with the following precedence order: +# 1. Config (set by fastappkit when running from core project) +# 2. DATABASE_URL environment variable (from .env file when running independently) +# 3. alembic.ini sqlalchemy.url (fallback) +# +# This allows the external app to work both: +# - When used in a core project (fastappkit sets it via config) +# - When developed independently (reads from .env file) database_url = config.get_main_option("sqlalchemy.url") if not database_url: - # If not set in config, try environment variable + # If not set in config, try environment variable (from .env) import os database_url = os.getenv("DATABASE_URL") if database_url: diff --git a/fastappkit/cli/templates/app/external/pyproject.toml.j2 b/fastappkit/cli/templates/app/external/pyproject.toml.j2 index 7a74a80..0519818 100644 --- a/fastappkit/cli/templates/app/external/pyproject.toml.j2 +++ b/fastappkit/cli/templates/app/external/pyproject.toml.j2 @@ -14,10 +14,12 @@ license = "MIT" packages = [{include = "{{ app_name }}"}] [tool.poetry.dependencies] -python = "^3.11" +python = ">=3.11,<4.0" fastapi = "*" sqlalchemy = "*" alembic = "*" +pydantic-settings = "*" +uvicorn = {extras = ["standard"], version = "*"} # Note: FastAppKit metadata is in {{ app_name }}/fastappkit.toml # This file is included in the package when published to PyPI diff --git a/fastappkit/cli/templates/project/pyproject.toml.j2 b/fastappkit/cli/templates/project/pyproject.toml.j2 index 1ca00dc..de5cdad 100644 --- a/fastappkit/cli/templates/project/pyproject.toml.j2 +++ b/fastappkit/cli/templates/project/pyproject.toml.j2 @@ -7,7 +7,7 @@ readme = "README.md" license = "MIT" [tool.poetry.dependencies] -python = "^3.11" +python = ">=3.11,<4.0" fastapi = "*" fastappkit = "*" {% if use_poetry %}uvicorn = {extras = ["standard"], version = "*"}{% endif %} diff --git a/poetry.lock b/poetry.lock index fbc5957..fcd2265 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1876,5 +1876,5 @@ files = [ [metadata] lock-version = "2.1" -python-versions = "^3.11.6" -content-hash = "45fc26d0962e7b4ce838d4e1425eff68e07bc8bb24bbef5f7e6238bcf4c78925" +python-versions = ">=3.11,<4.0" +content-hash = "4cbcebe5a48f8238a237b33a8e0abeb477fd2fc15d6ce38a9b8aa885a2374106" diff --git a/pyproject.toml b/pyproject.toml index d2df39f..095b2d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,7 +27,7 @@ include = [ ] [tool.poetry.dependencies] -python = "^3.11.6" +python = ">=3.11,<4.0" fastapi = ">=0.120.0,<0.130" sqlalchemy = ">=2.0,<3.0" alembic = ">=1.17.2,<1.18"