diff --git a/CLI.md b/CLI.md
new file mode 100644
index 0000000..d9f6158
--- /dev/null
+++ b/CLI.md
@@ -0,0 +1,339 @@
+# FastAPI Cruddy Framework CLI
+
+The FastAPI Cruddy Framework now includes a powerful CLI tool to help developers scaffold new projects and generate CRUD resources quickly and efficiently.
+
+## Installation
+
+The CLI is automatically available after installing the `fastapi-cruddy-framework` package:
+
+```bash
+pip install fastapi-cruddy-framework
+```
+
+## CLI Commands
+
+### Initialize a New Project
+
+Create a new FastAPI Cruddy Framework project with a single command:
+
+```bash
+cruddy init my_project
+```
+
+**Options:**
+- `--database, -d`: Choose database adapter (`sqlite`, `postgresql`, `mysql`) - default: `sqlite`
+- `--template, -t`: Choose project template (`minimal`, `full`) - default: `minimal`
+- `--directory`: Specify target directory - default: current directory
+
+**Examples:**
+
+```bash
+# Create a new SQLite project
+cruddy init my_blog --database sqlite
+
+# Create a PostgreSQL project
+cruddy init my_api --database postgresql
+
+# Create a project in a specific directory
+cruddy init my_project --directory /path/to/projects
+```
+
+### Generate Resources
+
+The CLI can generate complete CRUD resources or individual components.
+
+#### Generate a Complete Resource
+
+```bash
+cruddy generate resource User --fields "name:str,email:str,age:int"
+```
+
+This creates:
+- `models/user.py` - Database model with Create, Update, View, and Base classes
+- `resources/user.py` - Resource configuration connecting model, controller, and repository
+- `controllers/user.py` - Controller extension for custom endpoints
+
+**Options:**
+- `--id-type`: ID type for the resource (`int`, `uuid`, `str`) - default: `int`
+- `--fields`: Comma-separated field definitions (e.g., `"name:str,email:str,age:int"`)
+- `--relationships`: Comma-separated relationship definitions (e.g., `"posts:one-to-many,groups:many-to-many"`)
+
+**Field Types Supported:**
+- `str`, `string` - String fields
+- `int`, `integer` - Integer fields
+- `float` - Float fields
+- `bool`, `boolean` - Boolean fields
+- `datetime` - DateTime fields with timezone
+- `date` - Date fields
+- `time` - Time fields
+- `uuid` - UUID fields
+- `json` - JSON fields (uses `Any` type)
+- `text` - Text fields (alias for `str`)
+
+**Examples:**
+
+```bash
+# Basic user resource
+cruddy generate resource User --fields "name:str,email:str,age:int"
+
+# Blog post with UUID primary key
+cruddy generate resource Post --id-type uuid --fields "title:str,content:text,published:bool,created_at:datetime"
+
+# Product with relationships
+cruddy generate resource Product --fields "name:str,price:float,description:text" --relationships "reviews:one-to-many,categories:many-to-many"
+```
+
+#### Generate Individual Components
+
+**Generate a Model Only:**
+```bash
+cruddy generate model Product --fields "name:str,price:float,description:str"
+```
+
+**Generate a Controller Only:**
+```bash
+cruddy generate controller CustomEndpoints
+```
+
+## Project Structure
+
+A generated project follows this structure:
+
+```
+my_project/
+├── main.py # Application entry point
+├── requirements.txt # Python dependencies
+├── .env # Environment variables
+├── README.md # Project documentation
+├── adapters/ # Database adapter configuration
+│ └── __init__.py
+├── config/ # Application settings
+│ └── __init__.py
+├── controllers/ # Custom controller extensions
+│ └── __init__.py
+├── models/ # Database models
+│ └── __init__.py
+├── policies/ # Business logic policies
+│ └── __init__.py
+├── resources/ # Resource definitions
+│ └── __init__.py
+├── schemas/ # Response and validation schemas
+│ └── __init__.py
+└── utils/ # Utility functions
+ └── __init__.py
+```
+
+## Generated Code Examples
+
+### Model Example (`models/user.py`)
+
+```python
+"""
+User model for FastAPI Cruddy Framework
+"""
+from typing import Any
+from datetime import datetime
+from fastapi_cruddy_framework import (
+ CruddyModel,
+ CruddyIntIDModel,
+ CruddyCreatedUpdatedMixin,
+ Field,
+)
+
+
+class UserUpdate(CruddyModel):
+ """Update model for User - contains fields that can be updated."""
+ name: str
+ email: str
+ age: int
+
+
+class UserCreate(UserUpdate):
+ """Create model for User - extends update model with creation-only fields."""
+ pass
+
+
+class UserView(CruddyIntIDModel):
+ """View model for User - defines fields returned in API responses."""
+ name: str
+ email: str
+ age: int
+
+
+class User(CruddyCreatedUpdatedMixin(), UserCreate, table=True):
+ """Base User model with database table definition."""
+ pass
+```
+
+### Resource Example (`resources/user.py`)
+
+```python
+"""
+User resource for FastAPI Cruddy Framework
+"""
+from fastapi_cruddy_framework import Resource
+from ..adapters import adapter
+from ..models.user import (
+ User,
+ UserCreate,
+ UserUpdate,
+ UserView,
+)
+from ..controllers.user import UserController
+
+
+resource = Resource(
+ adapter=adapter,
+ id_type=int,
+ resource_model=User,
+ resource_create_model=UserCreate,
+ resource_update_model=UserUpdate,
+ response_schema=UserView,
+ controller_extension=UserController,
+ # Add your policies, lifecycle hooks, and other configurations here
+ # policies_universal=[example_policy],
+ # protected_relationships=["example_relation"],
+)
+```
+
+### Controller Example (`controllers/user.py`)
+
+```python
+"""
+User controller extensions for FastAPI Cruddy Framework
+"""
+from fastapi_cruddy_framework import CruddyController
+from fastapi import Depends
+
+
+class UserController(CruddyController):
+ """Extended controller for User resource."""
+
+ def setup(self):
+ """Setup custom routes and extend default CRUD functionality."""
+ # Example custom route:
+ @self.controller.get(
+ "/example",
+ summary="Example custom endpoint",
+ description="This is an example of how to add custom endpoints",
+ )
+ async def custom_example():
+ return {"message": "Hello from User!"}
+
+ # Example of extending default actions:
+ # original_create = self.actions.create
+ #
+ # async def enhanced_create(request, data):
+ # # Add custom logic before creation
+ # result = await original_create(request, data)
+ # # Add custom logic after creation
+ # return result
+ #
+ # self.actions.create = enhanced_create
+```
+
+## Getting Started
+
+1. **Create a new project:**
+ ```bash
+ cruddy init my_blog --database sqlite
+ cd my_blog
+ ```
+
+2. **Install dependencies:**
+ ```bash
+ pip install -r requirements.txt
+ ```
+
+3. **Generate a resource:**
+ ```bash
+ cruddy generate resource Post --fields "title:str,content:text,published:bool"
+ ```
+
+4. **Run the application:**
+ ```bash
+ python main.py
+ ```
+
+5. **Access the API documentation:**
+ ```
+ http://localhost:8000/docs
+ ```
+
+## Database Support
+
+The CLI supports three database adapters:
+
+### SQLite (Default)
+- Perfect for development and small applications
+- No additional setup required
+- File-based or in-memory options
+
+### PostgreSQL
+- Production-ready relational database
+- Requires PostgreSQL server and `psycopg2-binary`
+- Advanced features like full-text search
+
+### MySQL
+- Popular relational database
+- Requires MySQL server and `PyMySQL`
+- Good performance and scalability
+
+## Advanced Usage
+
+### Custom Policies
+Add business logic policies to your resources:
+
+```python
+# In your resource file
+from ..policies.auth import require_authentication
+
+resource = Resource(
+ # ... other config
+ policies_universal=[require_authentication],
+ policies_create=[additional_create_policy],
+)
+```
+
+### Lifecycle Hooks
+Add hooks to execute code before/after CRUD operations:
+
+```python
+async def before_user_create(data):
+ # Hash password before saving
+ data.password = hash_password(data.password)
+
+resource = Resource(
+ # ... other config
+ lifecycle_before_create=before_user_create,
+)
+```
+
+### Protected Relationships
+Prevent certain relationships from being modified via API:
+
+```python
+resource = Resource(
+ # ... other config
+ protected_relationships=["admin_notes"],
+ protected_create_relationships=["system_metadata"],
+)
+```
+
+## Tips and Best Practices
+
+1. **Start with the minimal template** - You can always add features later
+2. **Use meaningful field names** - They become your API field names
+3. **Consider ID types carefully** - UUIDs for public APIs, integers for internal tools
+4. **Add validation in your models** - Use Pydantic validators for business rules
+5. **Organize policies by function** - Keep authentication, authorization, and validation separate
+6. **Use lifecycle hooks for side effects** - Logging, notifications, caching, etc.
+7. **Test your resources** - The framework provides excellent test helpers
+
+## Need Help?
+
+- Check the main [FastAPI Cruddy Framework documentation](README.md)
+- Look at the [example server](examples/fastapi_cruddy_sqlite/) for advanced patterns
+- File issues on [GitHub](https://github.com/mdconaway/fastapi-cruddy-framework)
+
+The CLI tool makes it incredibly fast to scaffold production-ready CRUD applications with FastAPI. Happy coding!
diff --git a/README.md b/README.md
index f14bc73..c2bc712 100644
--- a/README.md
+++ b/README.md
@@ -27,6 +27,35 @@ Come for the GraphQL and websocket managers, stay for the CRUD!
(back to top)
+
+
+## 🚀 CLI Tools
+
+**NEW:** FastAPI Cruddy Framework now includes a powerful CLI tool to help developers scaffold new projects and generate CRUD resources with a single command!
+
+```bash
+# Create a new project
+cruddy init my_blog --database postgresql
+
+# Generate a complete CRUD resource
+cruddy generate resource User --fields "name:str,email:str,age:int"
+
+# Generate individual components
+cruddy generate model Product --fields "name:str,price:float"
+cruddy generate controller CustomEndpoints
+```
+
+### Key Features:
+- **Project Scaffolding**: Create complete FastAPI Cruddy projects with proper directory structure
+- **Resource Generation**: Generate models, controllers, and resource definitions automatically
+- **Database Support**: Templates for SQLite, PostgreSQL, and MySQL
+- **Modern Tooling**: Poetry-based projects with development dependencies pre-configured
+- **Field Types**: Support for strings, integers, floats, booleans, datetimes, UUIDs, and more
+
+📖 **[Complete CLI Documentation](CLI.md)**
+
+(back to top)
+
## Front-end Libraries
diff --git a/fastapi_cruddy_framework/__init__.py b/fastapi_cruddy_framework/__init__.py
index b125992..0cc9bff 100644
--- a/fastapi_cruddy_framework/__init__.py
+++ b/fastapi_cruddy_framework/__init__.py
@@ -3,6 +3,17 @@
# Love,
# A Sails / Ember lover.
# -----------------------------------------------------------------
+
+import warnings
+
+# Suppress Pydantic warnings about Strawberry GraphQL types
+warnings.filterwarnings(
+ "ignore",
+ message=".*is not a Python type.*Pydantic will allow any object with no validation.*",
+ category=UserWarning,
+ module="pydantic.*",
+)
+
from validator_collection import (
checkers as field_checkers,
validators as field_validators,
diff --git a/fastapi_cruddy_framework/cli/__init__.py b/fastapi_cruddy_framework/cli/__init__.py
new file mode 100644
index 0000000..0c54fea
--- /dev/null
+++ b/fastapi_cruddy_framework/cli/__init__.py
@@ -0,0 +1,6 @@
+"""
+FastAPI Cruddy Framework CLI
+
+This module provides command-line tools to help developers scaffold
+new cruddy projects and generate resources, models, and other primitives.
+"""
diff --git a/fastapi_cruddy_framework/cli/generators.py b/fastapi_cruddy_framework/cli/generators.py
new file mode 100644
index 0000000..54aba91
--- /dev/null
+++ b/fastapi_cruddy_framework/cli/generators.py
@@ -0,0 +1,266 @@
+"""
+Code generators for FastAPI Cruddy Framework
+"""
+
+import os
+from pathlib import Path
+from typing import List, Tuple
+from .templates import (
+ PROJECT_TEMPLATES,
+ MODEL_TEMPLATE,
+ RESOURCE_TEMPLATE,
+ CONTROLLER_TEMPLATE,
+)
+
+
+def scaffold_project(
+ project_name: str,
+ project_path: Path,
+ database: str = "sqlite",
+ template: str = "minimal",
+) -> None:
+ """Generate a new FastAPI Cruddy Framework project."""
+ project_path.mkdir(parents=True, exist_ok=True)
+
+ # Create the nested source directory structure
+ source_path = project_path / project_name
+ source_path.mkdir(exist_ok=True)
+
+ # Create __init__.py for the main module
+ (source_path / "__init__.py").touch()
+
+ # Create directory structure within the nested module
+ directories = [
+ "models",
+ "resources",
+ "controllers",
+ "config",
+ "adapters",
+ "policies",
+ "schemas",
+ "utils",
+ "router",
+ "services",
+ "middleware",
+ ]
+
+ for directory in directories:
+ (source_path / directory).mkdir(exist_ok=True)
+ (source_path / directory / "__init__.py").touch()
+
+ # Get template files based on selection
+ template_files = PROJECT_TEMPLATES[template][database]
+
+ # Write template files
+ for file_path, content in template_files.items():
+ # Root-level files (pyproject.toml, README.md, .env) go at project root
+ if file_path in ["pyproject.toml", "README.md", ".env"]:
+ full_path = project_path / file_path
+ else:
+ # All other files go in the nested source directory
+ full_path = source_path / file_path
+
+ full_path.parent.mkdir(parents=True, exist_ok=True)
+
+ # Replace placeholders in content
+ content = content.replace("{{PROJECT_NAME}}", project_name)
+ content = content.replace("{{PROJECT_NAME_UPPER}}", project_name.upper())
+ content = content.replace("{{PROJECT_NAME_LOWER}}", project_name.lower())
+
+ with open(full_path, "w") as f:
+ f.write(content)
+
+
+def generate_resource(
+ resource_name: str,
+ id_type: str = "int",
+ fields: List[Tuple[str, str]] | None = None,
+ relationships: List[Tuple[str, str]] | None = None,
+) -> None:
+ """Generate a complete resource with model, controller, and resource files."""
+ if fields is None:
+ fields = []
+ if relationships is None:
+ relationships = []
+
+ # Get the project module path
+ project_module_path = _get_project_module_path()
+ if not project_module_path:
+ raise Exception(
+ "Could not find project module directory. Make sure you're in a Cruddy project root."
+ )
+
+ # Generate model first
+ generate_model(resource_name, id_type, fields)
+
+ # Generate controller
+ generate_controller(resource_name)
+
+ # Generate resource file in the correct nested path
+ resource_path = project_module_path / "resources" / f"{resource_name.lower()}.py"
+
+ # Build import line for ID type
+ id_import = _get_id_type_import(id_type)
+ id_import_line = f", {id_import}" if id_import else ""
+
+ context = {
+ "resource_name": resource_name,
+ "resource_name_lower": resource_name.lower(),
+ "resource_name_upper": resource_name.upper(),
+ "id_type": id_import,
+ "id_type_name": _get_id_type_name(id_type),
+ "id_type_import_line": id_import_line,
+ }
+
+ content = RESOURCE_TEMPLATE.format(**context)
+
+ with open(resource_path, "w") as f:
+ f.write(content)
+
+
+def generate_model(
+ model_name: str, id_type: str = "int", fields: List[Tuple[str, str]] | None = None
+) -> None:
+ """Generate a model file."""
+ if fields is None:
+ fields = []
+
+ # Get the project module path
+ project_module_path = _get_project_module_path()
+ if not project_module_path:
+ raise Exception(
+ "Could not find project module directory. Make sure you're in a Cruddy project root."
+ )
+
+ model_path = project_module_path / "models" / f"{model_name.lower()}.py"
+
+ # Generate field definitions
+ field_defs = []
+ for field_name, field_type in fields:
+ field_def = _generate_field_definition(field_name, field_type)
+ field_defs.append(field_def)
+
+ fields_content = (
+ " " + "\n ".join(field_defs)
+ if field_defs
+ else " pass # Add your fields here"
+ )
+
+ # Build import line for ID type
+ id_import = _get_id_type_import(id_type)
+ id_type_import_line = f", {id_import}" if id_import else ""
+
+ context = {
+ "model_name": model_name,
+ "model_name_lower": model_name.lower(),
+ "model_name_upper": model_name.upper(),
+ "id_type_import": id_type_import_line,
+ "id_type_name": _get_id_type_name(id_type),
+ "base_class": _get_base_model_class(id_type),
+ "fields": fields_content,
+ }
+
+ content = MODEL_TEMPLATE.format(**context)
+
+ with open(model_path, "w") as f:
+ f.write(content)
+
+
+def generate_controller(controller_name: str) -> None:
+ """Generate a controller file."""
+ # Get the project module path
+ project_module_path = _get_project_module_path()
+ if not project_module_path:
+ raise Exception(
+ "Could not find project module directory. Make sure you're in a Cruddy project root."
+ )
+
+ controller_path = (
+ project_module_path / "controllers" / f"{controller_name.lower()}.py"
+ )
+
+ context = {
+ "controller_name": controller_name,
+ "controller_name_lower": controller_name.lower(),
+ "controller_name_upper": controller_name.upper(),
+ }
+
+ content = CONTROLLER_TEMPLATE.format(**context)
+
+ with open(controller_path, "w") as f:
+ f.write(content)
+
+
+def _get_project_module_path() -> Path | None:
+ """Find the project module path (the nested directory containing source code)."""
+ # Look for a nested directory that has the Cruddy structure
+ for item in Path(".").iterdir():
+ if item.is_dir() and not item.name.startswith("."):
+ nested_path = item
+ # Check if this nested directory has the Cruddy structure
+ if all(
+ (nested_path / d).exists()
+ for d in ["models", "resources", "controllers"]
+ ):
+ return nested_path
+ return None
+
+
+def _get_id_type_import(id_type: str) -> str:
+ """Get the import statement for the ID type."""
+ if id_type == "uuid":
+ return "UUID"
+ elif id_type == "str":
+ return "" # str is built-in
+ else: # int
+ return "" # int is built-in
+
+
+def _get_id_type_name(id_type: str) -> str:
+ """Get the type name for the ID."""
+ if id_type == "uuid":
+ return "UUID"
+ elif id_type == "str":
+ return "str"
+ else: # int
+ return "int"
+
+
+def _get_base_model_class(id_type: str) -> str:
+ """Get the base model class based on ID type."""
+ if id_type == "uuid":
+ return "CruddyUUIDModel"
+ elif id_type == "str":
+ return "CruddyStringIDModel"
+ else: # int
+ return "CruddyIntIDModel"
+
+
+def _generate_field_definition(field_name: str, field_type: str) -> str:
+ """Generate a field definition line."""
+ type_mapping = {
+ "str": "str",
+ "string": "str",
+ "int": "int",
+ "integer": "int",
+ "float": "float",
+ "bool": "bool",
+ "boolean": "bool",
+ "datetime": "datetime",
+ "date": "date",
+ "time": "time",
+ "uuid": "UUID",
+ "json": "Any",
+ "text": "str",
+ }
+
+ python_type = type_mapping.get(field_type.lower(), "str")
+
+ # Add optional type annotation for most fields
+ if field_type.lower() not in ["int", "str", "float", "bool"]:
+ python_type = f"{python_type} | None"
+ default = " = None"
+ else:
+ default = ""
+
+ return f"{field_name}: {python_type}{default}"
diff --git a/fastapi_cruddy_framework/cli/main.py b/fastapi_cruddy_framework/cli/main.py
new file mode 100644
index 0000000..597ea8f
--- /dev/null
+++ b/fastapi_cruddy_framework/cli/main.py
@@ -0,0 +1,252 @@
+"""
+Main CLI entry point for fastapi-cruddy-framework
+"""
+
+import warnings
+import click
+import os
+from pathlib import Path
+from typing import Optional
+
+# Suppress Pydantic warnings about Strawberry GraphQL types
+warnings.filterwarnings(
+ "ignore",
+ message=".*is not a Python type.*Pydantic will allow any object with no validation.*",
+ category=UserWarning,
+ module="pydantic.*",
+)
+
+from .generators import (
+ scaffold_project,
+ generate_resource,
+ generate_model,
+ generate_controller,
+)
+
+
+@click.group()
+@click.version_option(package_name="fastapi-cruddy-framework")
+def cli():
+ """FastAPI Cruddy Framework CLI - Scaffold and generate CRUD applications"""
+ pass
+
+
+@cli.command()
+@click.argument("project_name")
+@click.option(
+ "--database",
+ "-d",
+ type=click.Choice(["sqlite", "postgresql", "mysql"]),
+ default="sqlite",
+ help="Database adapter to use (default: sqlite)",
+)
+@click.option(
+ "--template",
+ "-t",
+ type=click.Choice(["minimal", "full"]),
+ default="minimal",
+ help="Project template to use (default: minimal)",
+)
+@click.option(
+ "--directory",
+ type=click.Path(),
+ help="Directory to create the project in (default: current directory)",
+)
+def init(project_name: str, database: str, template: str, directory: Optional[str]):
+ """Initialize a new FastAPI Cruddy Framework project."""
+ target_dir = Path(directory) if directory else Path.cwd()
+ project_path = target_dir / project_name
+
+ if project_path.exists():
+ click.echo(
+ click.style(f"Error: Directory '{project_path}' already exists!", fg="red")
+ )
+ return
+
+ click.echo(f"Creating new Cruddy project: {project_name}")
+ click.echo(f"Database: {database}")
+ click.echo(f"Template: {template}")
+ click.echo(f"Location: {project_path}")
+
+ try:
+ scaffold_project(project_name, project_path, database, template)
+ click.echo(
+ click.style(
+ f"✅ Successfully created project '{project_name}'!", fg="green"
+ )
+ )
+ click.echo("\nNext steps:")
+ click.echo(f" cd {project_name}")
+ click.echo(" poetry install")
+ click.echo(" poetry run python main.py")
+ except Exception as e:
+ click.echo(click.style(f"Error creating project: {e}", fg="red"))
+
+
+@cli.group()
+def generate():
+ """Generate various project components."""
+ pass
+
+
+@generate.command("resource")
+@click.argument("resource_name")
+@click.option(
+ "--id-type",
+ type=click.Choice(["int", "uuid", "str"]),
+ default="int",
+ help="ID type for the resource (default: int)",
+)
+@click.option(
+ "--fields",
+ help="Comma-separated list of fields (e.g., 'name:str,email:str,age:int')",
+)
+@click.option(
+ "--relationships",
+ help="Comma-separated list of relationships (e.g., 'posts:one-to-many,groups:many-to-many')",
+)
+def generate_resource_cmd(
+ resource_name: str,
+ id_type: str,
+ fields: Optional[str],
+ relationships: Optional[str],
+):
+ """Generate a complete resource with model, controller, and related files."""
+ if not _in_cruddy_project():
+ click.echo(
+ click.style(
+ "Error: Not in a Cruddy project directory. Run 'cruddy init' first.",
+ fg="red",
+ )
+ )
+ return
+
+ try:
+ field_list = _parse_fields(fields) if fields else []
+ relationship_list = _parse_relationships(relationships) if relationships else []
+
+ generate_resource(resource_name, id_type, field_list, relationship_list)
+ click.echo(
+ click.style(
+ f"✅ Successfully generated resource '{resource_name}'!", fg="green"
+ )
+ )
+ click.echo("\nGenerated files:")
+ click.echo(f" models/{resource_name.lower()}.py")
+ click.echo(f" resources/{resource_name.lower()}.py")
+ click.echo(f" controllers/{resource_name.lower()}.py")
+ except Exception as e:
+ click.echo(click.style(f"Error generating resource: {e}", fg="red"))
+
+
+@generate.command("model")
+@click.argument("model_name")
+@click.option(
+ "--id-type",
+ type=click.Choice(["int", "uuid", "str"]),
+ default="int",
+ help="ID type for the model (default: int)",
+)
+@click.option(
+ "--fields",
+ help="Comma-separated list of fields (e.g., 'name:str,email:str,age:int')",
+)
+def generate_model_cmd(model_name: str, id_type: str, fields: Optional[str]):
+ """Generate a model file."""
+ if not _in_cruddy_project():
+ click.echo(
+ click.style(
+ "Error: Not in a Cruddy project directory. Run 'cruddy init' first.",
+ fg="red",
+ )
+ )
+ return
+
+ try:
+ field_list = _parse_fields(fields) if fields else []
+ generate_model(model_name, id_type, field_list)
+ click.echo(
+ click.style(f"✅ Successfully generated model '{model_name}'!", fg="green")
+ )
+ click.echo(f"Generated file: models/{model_name.lower()}.py")
+ except Exception as e:
+ click.echo(click.style(f"Error generating model: {e}", fg="red"))
+
+
+@generate.command("controller")
+@click.argument("controller_name")
+def generate_controller_cmd(controller_name: str):
+ """Generate a controller file."""
+ if not _in_cruddy_project():
+ click.echo(
+ click.style(
+ "Error: Not in a Cruddy project directory. Run 'cruddy init' first.",
+ fg="red",
+ )
+ )
+ return
+
+ try:
+ generate_controller(controller_name)
+ click.echo(
+ click.style(
+ f"✅ Successfully generated controller '{controller_name}'!", fg="green"
+ )
+ )
+ click.echo(f"Generated file: controllers/{controller_name.lower()}.py")
+ except Exception as e:
+ click.echo(click.style(f"Error generating controller: {e}", fg="red"))
+
+
+def _in_cruddy_project() -> bool:
+ """Check if we're in a Cruddy project directory."""
+ # Check if we have pyproject.toml (indicating a project root)
+ if not Path("pyproject.toml").exists():
+ return False
+
+ # Look for a nested directory structure that indicates a Cruddy project
+ # Find any subdirectory that has the Cruddy structure
+ for item in Path(".").iterdir():
+ if item.is_dir() and not item.name.startswith("."):
+ nested_path = item
+ # Check if this nested directory has the Cruddy structure
+ if all(
+ (nested_path / d).exists()
+ for d in ["models", "resources", "controllers"]
+ ):
+ return True
+
+ return False
+
+
+def _parse_fields(fields_str: str) -> list[tuple[str, str]]:
+ """Parse field string into list of (name, type) tuples."""
+ fields = []
+ for field in fields_str.split(","):
+ if ":" in field:
+ name, field_type = field.strip().split(":", 1)
+ fields.append((name.strip(), field_type.strip()))
+ else:
+ fields.append((field.strip(), "str"))
+ return fields
+
+
+def _parse_relationships(relationships_str: str) -> list[tuple[str, str]]:
+ """Parse relationship string into list of (name, type) tuples."""
+ relationships = []
+ for rel in relationships_str.split(","):
+ if ":" in rel:
+ name, rel_type = rel.strip().split(":", 1)
+ relationships.append((name.strip(), rel_type.strip()))
+ else:
+ relationships.append((rel.strip(), "one-to-many"))
+ return relationships
+
+
+def main():
+ """Main CLI entry point."""
+ cli()
+
+
+if __name__ == "__main__":
+ main()
diff --git a/fastapi_cruddy_framework/cli/templates.py b/fastapi_cruddy_framework/cli/templates.py
new file mode 100644
index 0000000..bf2cc29
--- /dev/null
+++ b/fastapi_cruddy_framework/cli/templates.py
@@ -0,0 +1,1407 @@
+"""
+Templates for FastAPI Cruddy Framework CLI
+"""
+
+# Model template
+MODEL_TEMPLATE = '''"""
+{model_name} model for FastAPI Cruddy Framework
+"""
+from typing import Any{id_type_import}
+from datetime import datetime
+from fastapi_cruddy_framework import (
+ CruddyModel,
+ {base_class},
+ CruddyCreatedUpdatedMixin,
+ Field,
+)
+
+
+class {model_name}Update(CruddyModel):
+ """Update model for {model_name} - contains fields that can be updated."""
+{fields}
+
+
+class {model_name}Create({model_name}Update):
+ """Create model for {model_name} - extends update model with creation-only fields."""
+ pass
+
+
+class {model_name}View({base_class}):
+ """View model for {model_name} - defines fields returned in API responses."""
+{fields}
+
+
+class {model_name}(CruddyCreatedUpdatedMixin(), {model_name}Create, table=True):
+ """Base {model_name} model with database table definition."""
+ pass
+'''
+
+# Resource template
+RESOURCE_TEMPLATE = '''"""
+{resource_name} resource for FastAPI Cruddy Framework
+"""
+from fastapi_cruddy_framework import Resource{id_type_import_line}
+from ..adapters import adapter # Import your configured adapter
+from ..models.{resource_name_lower} import (
+ {resource_name},
+ {resource_name}Create,
+ {resource_name}Update,
+ {resource_name}View,
+)
+from ..controllers.{resource_name_lower} import {resource_name}Controller
+
+
+resource = Resource(
+ adapter=adapter,
+ id_type={id_type_name},
+ resource_model={resource_name},
+ resource_create_model={resource_name}Create,
+ resource_update_model={resource_name}Update,
+ response_schema={resource_name}View,
+ controller_extension={resource_name}Controller,
+ # Add your policies, lifecycle hooks, and other configurations here
+ # policies_universal=[example_policy],
+ # protected_relationships=["example_relation"],
+)
+'''
+
+# Controller template
+CONTROLLER_TEMPLATE = '''"""
+{controller_name} controller extensions for FastAPI Cruddy Framework
+"""
+from fastapi_cruddy_framework import CruddyController
+from fastapi import Depends
+
+
+class {controller_name}Controller(CruddyController):
+ """Extended controller for {controller_name} resource."""
+
+ def setup(self):
+ """Setup custom routes and extend default CRUD functionality."""
+ # Access available properties:
+ # - self.actions (CRUD actions)
+ # - self.resource (Resource instance)
+ # - self.repository (Repository instance)
+ # - self.adapter (Database adapter)
+ # - self.controller (FastAPI router)
+
+ # Example custom route:
+ @self.controller.get(
+ "/example",
+ summary="Example custom endpoint",
+ description="This is an example of how to add custom endpoints",
+ )
+ async def custom_example():
+ return {{"message": "Hello from {controller_name}!"}}
+
+ # Example of extending default actions:
+ # original_create = self.actions.create
+ #
+ # async def enhanced_create(request, data):
+ # # Add custom logic before creation
+ # result = await original_create(request, data)
+ # # Add custom logic after creation
+ # return result
+ #
+ # self.actions.create = enhanced_create
+'''
+
+# Project templates
+MINIMAL_SQLITE_TEMPLATE = {
+ "main.py": '''"""
+FastAPI Cruddy Framework Application
+"""
+import logging
+from contextlib import asynccontextmanager
+from fastapi import FastAPI, status
+from fastapi.responses import JSONResponse
+from fastapi_cruddy_framework import CruddyNoMatchingRowException
+from starlette.middleware.cors import CORSMiddleware
+from sqlalchemy.exc import IntegrityError
+from starlette_session import SessionMiddleware
+from datetime import timedelta
+from .adapters import adapter
+from .config import general, http, sessions
+from .router import application as application_router
+
+logger = logging.getLogger(__name__)
+HTTP_400_BAD_REQUEST = status.HTTP_400_BAD_REQUEST
+HTTP_404_NOT_FOUND = status.HTTP_404_NOT_FOUND
+
+
+async def bootstrap(application: FastAPI):
+ """Bootstrap the application."""
+ # Because of how fastapi and sqlalchemy populate the relationship mappers, the CRUD router
+ # can't be fully loaded until after the fastapi server starts. Make sure you only mount
+ # the application_router in the bootstrapper. Fortunately, routers can be added lazily, which
+ # forces fastapi to re-index the routes and update the openapi.json.
+ await adapter.destroy_then_create_all_tables_unsafe()
+ application.include_router(application_router.router)
+ logger.info(f"{general.PROJECT_NAME}, {general.API_VERSION}: Bootstrap complete")
+
+
+async def shutdown():
+ """Application shutdown handler."""
+ logger.info(f"{general.PROJECT_NAME}: Shutdown complete")
+
+
+@asynccontextmanager
+async def lifespan(application: FastAPI):
+ await bootstrap(application)
+ yield
+ await shutdown()
+
+
+app = FastAPI(
+ title=general.PROJECT_NAME,
+ version=general.API_VERSION,
+ lifespan=lifespan
+)
+
+# Set all CORS origins enabled
+if http.HTTP_CORS_ORIGINS:
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=[str(origin) for origin in http.HTTP_CORS_ORIGINS],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+# Add session storage/retrieval to incoming requests
+app.add_middleware(
+ SessionMiddleware,
+ secret_key=str(sessions.SESSION_SECRET_KEY),
+ cookie_name=sessions.SESSION_COOKIE_NAME,
+ https_only=False,
+ same_site="lax", # lax or strict
+ max_age=int(timedelta(days=sessions.SESSION_MAX_AGE).total_seconds()), # in seconds
+)
+
+
+# Add global handler to catch DB integrity errors
+@app.exception_handler(IntegrityError)
+async def integrity_exception_handler(_, exc: IntegrityError):
+ return JSONResponse(
+ status_code=HTTP_400_BAD_REQUEST,
+ content={"detail": [str(exc.orig)]},
+ )
+
+
+@app.exception_handler(CruddyNoMatchingRowException)
+async def no_row_exception_handler(_, exc: CruddyNoMatchingRowException):
+ return JSONResponse(
+ status_code=HTTP_404_NOT_FOUND,
+ content={"detail": [str(exc)]},
+ )
+
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run("{{PROJECT_NAME_LOWER}}.main:app", host="0.0.0.0", port=http.HTTP_PORT, reload=True)
+''',
+ "router/__init__.py": '''"""
+Router modules for {{PROJECT_NAME}}
+"""
+''',
+ "router/application.py": '''"""
+Main application router for {{PROJECT_NAME}}
+"""
+from logging import getLogger
+from fastapi import APIRouter
+from fastapi_cruddy_framework import CreateRouterFromResources, CruddyResourceRegistry
+import {{PROJECT_NAME_LOWER}}
+
+logger = getLogger(__name__)
+
+# Create the main application router from resources
+router: APIRouter = CreateRouterFromResources(
+ application_module={{PROJECT_NAME_LOWER}},
+ resource_path="resources"
+)
+
+
+@router.get("/health", tags=["application"])
+async def health_check() -> bool:
+ """Health check endpoint - returns True when the application is ready."""
+ return CruddyResourceRegistry.is_ready()
+
+
+# You can add additional routes to this router below
+# For example:
+# @router.get("/custom", tags=["custom"])
+# async def custom_endpoint():
+# return {"message": "Custom endpoint"}
+''',
+ "bootloader.py": """import uvicorn
+from {{PROJECT_NAME_LOWER}}.config import http
+
+
+def start():
+ uvicorn.run(
+ "{{PROJECT_NAME_LOWER}}.main:app",
+ host="0.0.0.0",
+ port=http.HTTP_PORT,
+ reload=True,
+ )
+""",
+ "adapters/__init__.py": '''"""
+Database adapters for {{PROJECT_NAME}}
+"""
+from .application import adapter
+
+__all__ = ["adapter"]
+''',
+ "adapters/application.py": '''"""
+Application database adapter for {{PROJECT_NAME}}
+"""
+from fastapi import Request
+from fastapi_cruddy_framework import SqliteAdapter
+from sqlmodel.ext.asyncio.session import AsyncSession
+
+
+async def session_setup(session: AsyncSession, request: Request):
+ """
+ Setup database session for each request.
+
+ Here you can set roles and session values on the database layer session.
+ This allows you to propagate user identity information all the way into
+ the database to perform row-level data security.
+
+ Since you can access a request object here, you can scope a database
+ role to the specific user launching the HTTP request!
+
+ NOTE: Calling AbstractRepository functions WITHOUT sending the request value
+ WILL bypass this session setup function. This allows an app to still perform
+ root level queries when needed. All auto-generated routes WILL enforce
+ role-scope via this hook as the HTTP endpoint will always pass the request.
+
+ Args:
+ session: The database session
+ request: The HTTP request object
+ """
+ assert isinstance(request, Request)
+ assert isinstance(session, AsyncSession)
+ # Add your session setup logic here
+
+
+async def session_teardown(session: AsyncSession, request: Request):
+ """
+ Cleanup database session after each request.
+
+ Here you can tear-down any roles/settings you setup on a per-session basis.
+
+ Args:
+ session: The database session
+ request: The HTTP request object
+ """
+ assert isinstance(request, Request)
+ assert isinstance(session, AsyncSession)
+ # Add your session teardown logic here
+
+
+# Initialize the application database adapter
+adapter = SqliteAdapter(
+ mode="memory", # Change to "file" and set db_path for persistent storage
+ # db_path="./{{PROJECT_NAME_LOWER}}.db",
+ session_setup=session_setup,
+ session_teardown=session_teardown,
+)
+''',
+ "config/__init__.py": '''"""
+Configuration settings for {{PROJECT_NAME}}
+"""
+from .general import general
+from .http import http
+from .sessions import sessions
+
+__all__ = ["general", "http", "sessions"]
+''',
+ "config/_base.py": """import os
+from pydantic_settings import BaseSettings
+
+
+class Base(BaseSettings):
+ class Config:
+ case_sensitive = True
+ env_file = os.path.expanduser("~/.env")
+ env_file_encoding = "utf-8"
+""",
+ "config/general.py": """from {{PROJECT_NAME_LOWER}}.config._base import Base
+
+
+class General(Base):
+ PROJECT_NAME: str = "{{PROJECT_NAME}}"
+ API_VERSION: str = "1.0.0"
+ DEFAULT_LIMIT: int = 20
+
+
+general = General()
+""",
+ "config/http.py": """from {{PROJECT_NAME_LOWER}}.config._base import Base
+from pydantic import model_validator, AnyHttpUrl
+
+
+class Http(Base):
+ HTTP_PORT: int = 8000
+ HTTP_CORS_ORIGINS: str | list[str] | list[AnyHttpUrl] = ["*"]
+
+ @model_validator(mode="after")
+ def assemble_cors_origins(self):
+ if isinstance(
+ self.HTTP_CORS_ORIGINS, str
+ ) and not self.HTTP_CORS_ORIGINS.startswith("["):
+ self.HTTP_CORS_ORIGINS = [
+ i.strip() for i in self.HTTP_CORS_ORIGINS.split(",")
+ ]
+ elif isinstance(self.HTTP_CORS_ORIGINS, list):
+ self.HTTP_CORS_ORIGINS = self.HTTP_CORS_ORIGINS
+ else:
+ raise ValueError(self.HTTP_CORS_ORIGINS)
+ return self
+
+
+http = Http()
+""",
+ "config/sessions.py": """from {{PROJECT_NAME_LOWER}}.config._base import Base
+from pydantic import model_validator
+from secrets import token_urlsafe
+
+
+class Sessions(Base):
+ SESSION_COOKIE_NAME: str = "{{PROJECT_NAME_LOWER}}"
+ SESSION_SECRET_KEY: str | None = None
+ SESSION_MAX_AGE: int = 1
+
+ @model_validator(mode="after")
+ def validate_session_secret(self):
+ self.SESSION_SECRET_KEY = (
+ self.SESSION_SECRET_KEY
+ if isinstance(self.SESSION_SECRET_KEY, str)
+ else token_urlsafe(32)
+ )
+ return self
+
+
+sessions = Sessions()
+""",
+ "pyproject.toml": """[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "{{PROJECT_NAME_LOWER}}"
+version = "0.1.0"
+description = "A FastAPI Cruddy Framework application"
+authors = ["Your Name "]
+readme = "README.md"
+packages = [{include = "{{PROJECT_NAME_LOWER}}"}]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+fastapi = {extras = ["all"], version = "^0.115.13"}
+fastapi-cruddy-framework = "^1.10.0"
+uvicorn = {extras = ["standard"], version = "^0.32.0"}
+pydantic-settings = "^2.0.0"
+aiosqlite = "^0.20.0"
+starlette-session = "^0.4.3"
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.0.0"
+pytest-asyncio = "^0.23.0"
+black = "^24.0.0"
+ruff = "^0.1.0"
+
+[tool.poetry.scripts]
+start = "{{PROJECT_NAME_LOWER}}.bootloader:start"
+
+[tool.black]
+line-length = 88
+target-version = ['py310']
+
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+select = ["E", "W", "F", "I"]
+ignore = ["E501"]
+""",
+ ".env": """# Environment variables for {{PROJECT_NAME}}
+DEBUG=true
+DATABASE_URL=sqlite:///./{{PROJECT_NAME_LOWER}}.db
+""",
+ "README.md": """# {{PROJECT_NAME}}
+
+A FastAPI Cruddy Framework application.
+
+## Setup
+
+1. Install dependencies using Poetry:
+ ```bash
+ poetry install
+ ```
+
+2. Run the application:
+ ```bash
+ poetry run python main.py
+ # OR
+ poetry shell
+ python main.py
+ # OR use the poetry script
+ poetry run start
+ ```
+
+3. Access the API documentation at http://localhost:8000/docs
+
+## Usage
+
+### Generate a new resource:
+```bash
+cruddy generate resource User --fields "name:str,email:str,age:int"
+```
+
+### Generate just a model:
+```bash
+cruddy generate model Product --fields "name:str,price:float,description:str"
+```
+
+### Generate a controller:
+```bash
+cruddy generate controller CustomEndpoints
+```
+
+## Development
+
+### Code formatting:
+```bash
+poetry run black .
+```
+
+### Linting:
+```bash
+poetry run ruff check .
+```
+
+### Tests:
+```bash
+poetry run pytest
+```
+
+## Project Structure
+
+- `models/` - Database models
+- `resources/` - Resource definitions (combines models, controllers, repositories)
+- `controllers/` - Custom controller extensions
+- `config/` - Configuration settings
+- `adapters/` - Database adapter configuration
+- `policies/` - Business logic policies
+- `schemas/` - Response and validation schemas
+""",
+}
+
+MINIMAL_POSTGRESQL_TEMPLATE = {
+ "main.py": MINIMAL_SQLITE_TEMPLATE["main.py"],
+ "bootloader.py": MINIMAL_SQLITE_TEMPLATE["bootloader.py"],
+ "adapters/__init__.py": '''"""
+Database adapters for {{PROJECT_NAME}}
+"""
+from .application import adapter
+
+__all__ = ["adapter"]
+''',
+ "adapters/application.py": '''"""
+Application database adapter for {{PROJECT_NAME}}
+"""
+from fastapi import Request
+from fastapi_cruddy_framework import PostgresqlAdapter
+from sqlmodel.ext.asyncio.session import AsyncSession
+
+
+async def session_setup(session: AsyncSession, request: Request):
+ """
+ Setup database session for each request.
+
+ Here you can set roles and session values on the database layer session.
+ This allows you to propagate user identity information all the way into
+ the database to perform row-level data security.
+
+ Since you can access a request object here, you can scope a database
+ role to the specific user launching the HTTP request!
+
+ NOTE: Calling AbstractRepository functions WITHOUT sending the request value
+ WILL bypass this session setup function. This allows an app to still perform
+ root level queries when needed. All auto-generated routes WILL enforce
+ role-scope via this hook as the HTTP endpoint will always pass the request.
+
+ Args:
+ session: The database session
+ request: The HTTP request object
+ """
+ assert isinstance(request, Request)
+ assert isinstance(session, AsyncSession)
+ # Add your session setup logic here
+
+
+async def session_teardown(session: AsyncSession, request: Request):
+ """
+ Cleanup database session after each request.
+
+ Here you can tear-down any roles/settings you setup on a per-session basis.
+
+ Args:
+ session: The database session
+ request: The HTTP request object
+ """
+ assert isinstance(request, Request)
+ assert isinstance(session, AsyncSession)
+ # Add your session teardown logic here
+
+
+# Initialize the application database adapter
+adapter = PostgresqlAdapter(
+ connection_uri="postgresql://user:password@localhost/{{PROJECT_NAME_LOWER}}",
+ pool_size=5,
+ max_overflow=10,
+ session_setup=session_setup,
+ session_teardown=session_teardown,
+)
+''',
+ "config/__init__.py": MINIMAL_SQLITE_TEMPLATE["config/__init__.py"],
+ "config/_base.py": MINIMAL_SQLITE_TEMPLATE["config/_base.py"],
+ "config/general.py": MINIMAL_SQLITE_TEMPLATE["config/general.py"],
+ "config/http.py": MINIMAL_SQLITE_TEMPLATE["config/http.py"],
+ "config/sessions.py": MINIMAL_SQLITE_TEMPLATE["config/sessions.py"],
+ "router/__init__.py": MINIMAL_SQLITE_TEMPLATE["router/__init__.py"],
+ "router/application.py": MINIMAL_SQLITE_TEMPLATE["router/application.py"],
+ "pyproject.toml": """[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "{{PROJECT_NAME_LOWER}}"
+version = "0.1.0"
+description = "A FastAPI Cruddy Framework application"
+authors = ["Your Name "]
+readme = "README.md"
+packages = [{include = "{{PROJECT_NAME_LOWER}}"}]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+fastapi = {extras = ["all"], version = "^0.115.13"}
+fastapi-cruddy-framework = "^1.10.0"
+uvicorn = {extras = ["standard"], version = "^0.32.0"}
+pydantic-settings = "^2.0.0"
+asyncpg = "^0.30.0"
+starlette-session = "^0.4.3"
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.0.0"
+pytest-asyncio = "^0.23.0"
+black = "^24.0.0"
+ruff = "^0.1.0"
+
+[tool.poetry.scripts]
+start = "{{PROJECT_NAME_LOWER}}.bootloader:start"
+
+[tool.black]
+line-length = 88
+target-version = ['py310']
+
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+select = ["E", "W", "F", "I"]
+ignore = ["E501"]
+""",
+ ".env": """# Environment variables for {{PROJECT_NAME}}
+DEBUG=true
+DATABASE_URL=postgresql://user:password@localhost/{{PROJECT_NAME_LOWER}}
+""",
+ "README.md": MINIMAL_SQLITE_TEMPLATE["README.md"],
+}
+
+MINIMAL_MYSQL_TEMPLATE = {
+ "main.py": MINIMAL_SQLITE_TEMPLATE["main.py"],
+ "bootloader.py": MINIMAL_SQLITE_TEMPLATE["bootloader.py"],
+ "adapters/__init__.py": '''"""
+Database adapters for {{PROJECT_NAME}}
+"""
+from .application import adapter
+
+__all__ = ["adapter"]
+''',
+ "adapters/application.py": '''"""
+Application database adapter for {{PROJECT_NAME}}
+"""
+from fastapi import Request
+from fastapi_cruddy_framework import MysqlAdapter
+from sqlmodel.ext.asyncio.session import AsyncSession
+
+
+async def session_setup(session: AsyncSession, request: Request):
+ """
+ Setup database session for each request.
+
+ Here you can set roles and session values on the database layer session.
+ This allows you to propagate user identity information all the way into
+ the database to perform row-level data security.
+
+ Since you can access a request object here, you can scope a database
+ role to the specific user launching the HTTP request!
+
+ NOTE: Calling AbstractRepository functions WITHOUT sending the request value
+ WILL bypass this session setup function. This allows an app to still perform
+ root level queries when needed. All auto-generated routes WILL enforce
+ role-scope via this hook as the HTTP endpoint will always pass the request.
+
+ Args:
+ session: The database session
+ request: The HTTP request object
+ """
+ assert isinstance(request, Request)
+ assert isinstance(session, AsyncSession)
+ # Add your session setup logic here
+
+
+async def session_teardown(session: AsyncSession, request: Request):
+ """
+ Cleanup database session after each request.
+
+ Here you can tear-down any roles/settings you setup on a per-session basis.
+
+ Args:
+ session: The database session
+ request: The HTTP request object
+ """
+ assert isinstance(request, Request)
+ assert isinstance(session, AsyncSession)
+ # Add your session teardown logic here
+
+
+# Initialize the application database adapter
+adapter = MysqlAdapter(
+ connection_uri="mysql://user:password@localhost/{{PROJECT_NAME_LOWER}}",
+ pool_size=5,
+ max_overflow=10,
+ session_setup=session_setup,
+ session_teardown=session_teardown,
+)
+''',
+ "config/__init__.py": MINIMAL_SQLITE_TEMPLATE["config/__init__.py"],
+ "config/_base.py": MINIMAL_SQLITE_TEMPLATE["config/_base.py"],
+ "config/general.py": MINIMAL_SQLITE_TEMPLATE["config/general.py"],
+ "config/http.py": MINIMAL_SQLITE_TEMPLATE["config/http.py"],
+ "config/sessions.py": MINIMAL_SQLITE_TEMPLATE["config/sessions.py"],
+ "router/__init__.py": MINIMAL_SQLITE_TEMPLATE["router/__init__.py"],
+ "router/application.py": MINIMAL_SQLITE_TEMPLATE["router/application.py"],
+ "pyproject.toml": """[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "{{PROJECT_NAME_LOWER}}"
+version = "0.1.0"
+description = "A FastAPI Cruddy Framework application"
+authors = ["Your Name "]
+readme = "README.md"
+packages = [{include = "{{PROJECT_NAME_LOWER}}"}]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+fastapi = {extras = ["all"], version = "^0.115.13"}
+fastapi-cruddy-framework = "^1.10.0"
+uvicorn = {extras = ["standard"], version = "^0.32.0"}
+pydantic-settings = "^2.0.0"
+PyMySQL = "^1.0.0"
+starlette-session = "^0.4.3"
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.0.0"
+pytest-asyncio = "^0.23.0"
+black = "^24.0.0"
+ruff = "^0.1.0"
+
+[tool.poetry.scripts]
+start = "{{PROJECT_NAME_LOWER}}.bootloader:start"
+
+[tool.black]
+line-length = 88
+target-version = ['py310']
+
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+select = ["E", "W", "F", "I"]
+ignore = ["E501"]
+""",
+ ".env": """# Environment variables for {{PROJECT_NAME}}
+DEBUG=true
+DATABASE_URL=mysql://user:password@localhost/{{PROJECT_NAME_LOWER}}
+""",
+ "README.md": MINIMAL_SQLITE_TEMPLATE["README.md"],
+}
+
+# Full templates with auth, websockets, GraphQL, and middleware
+FULL_SQLITE_TEMPLATE = {
+ **MINIMAL_SQLITE_TEMPLATE,
+ # Enhanced main.py with websocket managers
+ "main.py": '''"""
+FastAPI Cruddy Framework Application with Auth, Websockets, and GraphQL
+"""
+import logging
+from contextlib import asynccontextmanager
+from fastapi import FastAPI, status
+from fastapi.responses import JSONResponse
+from fastapi_cruddy_framework import CruddyNoMatchingRowException
+from starlette.middleware.cors import CORSMiddleware
+from sqlalchemy.exc import IntegrityError
+from starlette_session import SessionMiddleware
+from datetime import timedelta
+from .adapters import adapter
+from .config import general, http, sessions
+from .router import application as application_router
+from .services.websocket_manager import websocket_manager
+from .middleware.request_logger import RequestLogger
+
+logger = logging.getLogger(__name__)
+HTTP_400_BAD_REQUEST = status.HTTP_400_BAD_REQUEST
+HTTP_404_NOT_FOUND = status.HTTP_404_NOT_FOUND
+
+
+async def bootstrap(application: FastAPI):
+ """Bootstrap the application."""
+ # Because of how fastapi and sqlalchemy populate the relationship mappers, the CRUD router
+ # can't be fully loaded until after the fastapi server starts. Make sure you only mount
+ # the application_router in the bootstrapper. Fortunately, routers can be added lazily, which
+ # forces fastapi to re-index the routes and update the openapi.json.
+ await adapter.destroy_then_create_all_tables_unsafe()
+
+ # Start websocket manager
+ await websocket_manager.startup()
+
+ application.include_router(application_router.router)
+ logger.info(f"{general.PROJECT_NAME}, {general.API_VERSION}: Bootstrap complete")
+
+
+async def shutdown():
+ """Application shutdown handler."""
+ await websocket_manager.dispose()
+ logger.info(f"{general.PROJECT_NAME}: Shutdown complete")
+
+
+@asynccontextmanager
+async def lifespan(application: FastAPI):
+ await bootstrap(application)
+ yield
+ await shutdown()
+
+
+app = FastAPI(
+ title=general.PROJECT_NAME,
+ version=general.API_VERSION,
+ lifespan=lifespan
+)
+
+# Add request logging middleware
+app.add_middleware(RequestLogger)
+
+# Set all CORS origins enabled
+if http.HTTP_CORS_ORIGINS:
+ app.add_middleware(
+ CORSMiddleware,
+ allow_origins=[str(origin) for origin in http.HTTP_CORS_ORIGINS],
+ allow_credentials=True,
+ allow_methods=["*"],
+ allow_headers=["*"],
+ )
+
+# Add session storage/retrieval to incoming requests
+app.add_middleware(
+ SessionMiddleware,
+ secret_key=str(sessions.SESSION_SECRET_KEY),
+ cookie_name=sessions.SESSION_COOKIE_NAME,
+ https_only=False,
+ same_site="lax", # lax or strict
+ max_age=int(timedelta(days=sessions.SESSION_MAX_AGE).total_seconds()), # in seconds
+)
+
+
+# Add global handler to catch DB integrity errors
+@app.exception_handler(IntegrityError)
+async def integrity_exception_handler(_, exc: IntegrityError):
+ return JSONResponse(
+ status_code=HTTP_400_BAD_REQUEST,
+ content={"detail": [str(exc.orig)]},
+ )
+
+
+@app.exception_handler(CruddyNoMatchingRowException)
+async def no_row_exception_handler(_, exc: CruddyNoMatchingRowException):
+ return JSONResponse(
+ status_code=HTTP_404_NOT_FOUND,
+ content={"detail": [str(exc)]},
+ )
+
+
+if __name__ == "__main__":
+ import uvicorn
+ uvicorn.run("{{PROJECT_NAME_LOWER}}.main:app", host="0.0.0.0", port=http.HTTP_PORT, reload=True)
+''',
+ # Enhanced router with websocket endpoints
+ "router/application.py": '''"""
+Main application router for {{PROJECT_NAME}} with Websockets
+"""
+from logging import getLogger
+from fastapi import APIRouter, WebSocket
+from fastapi_cruddy_framework import (
+ CreateRouterFromResources,
+ CruddyResourceRegistry,
+ uuid7,
+ dependency_list
+)
+from ..policies.verify_session import verify_session
+from ..policies.naive_auth import naive_auth
+from ..services.websocket_manager import websocket_manager
+import {{PROJECT_NAME_LOWER}}
+
+logger = getLogger(__name__)
+
+# Create the main application router from resources
+router: APIRouter = CreateRouterFromResources(
+ application_module={{PROJECT_NAME_LOWER}},
+ resource_path="resources"
+)
+
+
+@router.get("/health", tags=["application"])
+async def health_check() -> bool:
+ """Health check endpoint - returns True when the application is ready."""
+ return CruddyResourceRegistry.is_ready()
+
+
+# Websocket endpoint with authentication
+@router.websocket("/ws", dependencies=dependency_list(verify_session, naive_auth))
+async def websocket_endpoint(websocket: WebSocket):
+ """Main websocket endpoint with authentication and session management."""
+ override_socket_id = str(uuid7())
+
+ async with websocket_manager.connect(
+ websocket,
+ override_socket_id=override_socket_id,
+ disconnect_message_type="socket_disconnect",
+ disconnect_message_data={
+ "socket_id": f"{override_socket_id}",
+ "message": f"Websocket client {override_socket_id} disconnected",
+ },
+ ) as socket_id:
+ logger.info("Socket %s connected", socket_id)
+ await websocket_manager.broadcast(
+ type="socket_connect",
+ data={"socket_id": socket_id}
+ )
+
+
+# You can add additional routes to this router below
+# For example:
+# @router.get("/custom", tags=["custom"])
+# async def custom_endpoint():
+# return {"message": "Custom endpoint"}
+''',
+ # Auth policies
+ "policies/verify_session.py": '''"""
+Session verification policy for {{PROJECT_NAME}}
+"""
+from fastapi import HTTPException
+from fastapi.requests import HTTPConnection
+
+
+async def verify_session(connection: HTTPConnection):
+ """Verify that a valid session exists."""
+ if not isinstance(connection.session, dict):
+ raise HTTPException(status_code=400, detail="session does not exist!")
+''',
+ "policies/naive_auth.py": '''"""
+Authentication policy for {{PROJECT_NAME}}
+"""
+from __future__ import annotations
+from typing_extensions import Annotated
+from fastapi import Security, Query, HTTPException, status
+from fastapi_cruddy_framework import CruddyHTTPBearer
+from fastapi.security import HTTPAuthorizationCredentials
+from fastapi.requests import HTTPConnection
+
+
+HTTP_403_FORBIDDEN = status.HTTP_403_FORBIDDEN
+
+
+async def naive_auth(
+ connection: HTTPConnection,
+ credentials: HTTPAuthorizationCredentials | None = Security(
+ CruddyHTTPBearer(auto_error=False)
+ ),
+ auth_token: Annotated[str | None, Query()] = None,
+):
+ """Authenticate users via Bearer token or query parameter."""
+ if connection.session.get("token") is not None:
+ return
+
+ if (
+ not credentials
+ or not hasattr(credentials, "scheme")
+ or credentials.scheme != "Bearer"
+ ):
+ if isinstance(auth_token, str):
+ token = auth_token
+ else:
+ token = None
+ else:
+ token = credentials.credentials
+
+ if not token:
+ raise HTTPException(
+ status_code=HTTP_403_FORBIDDEN,
+ detail="Authentication required - provide Bearer token or auth_token query parameter",
+ )
+
+ connection.session["token"] = token
+''',
+ # Websocket services
+ "services/__init__.py": '''"""
+Service modules for {{PROJECT_NAME}}
+"""
+''',
+ "services/websocket_manager.py": '''"""
+Websocket connection manager for {{PROJECT_NAME}}
+"""
+from typing import Any
+from logging import getLogger
+from fastapi import WebSocket
+from fastapi_cruddy_framework import (
+ WebsocketConnectionManager,
+ CLIENT_MESSAGE_EVENT,
+ get_state,
+)
+from ..schemas.client_message import ClientMessage, ClientControlWithTarget
+
+logger = getLogger(__name__)
+
+# Initialize websocket manager (uses in-memory Redis for development)
+websocket_manager = WebsocketConnectionManager(
+ redis_mode="memory", # Change to "connection" for production Redis
+ redis_channel="{{PROJECT_NAME_LOWER}}_websockets"
+)
+
+
+async def client_controls(websocket: WebSocket, message: ClientMessage):
+ """Handle websocket control messages like join/leave room, kill socket, etc."""
+ try:
+ control_message = ClientControlWithTarget.model_validate(message.model_dump())
+ except Exception as e:
+ logger.info(
+ "Discarding message %s, due to missing params: %s", message.model_dump(), e
+ )
+ return
+
+ socket_id = str(get_state(websocket, "socket_id", ""))
+
+ if control_message.type == "client_join_room":
+ await websocket_manager.join_room_by_socket_id(
+ id=socket_id, room_id=control_message.target
+ )
+ elif control_message.type == "client_leave_room":
+ await websocket_manager.leave_room_by_socket_id(
+ id=socket_id, room_id=control_message.target
+ )
+ elif control_message.type == "client_kill_socket_id":
+ await websocket_manager.kill_sockets_by_socket_id(id=control_message.target)
+ elif control_message.type == "client_kill_room":
+ await websocket_manager.kill_room_by_id(room_id=control_message.target)
+ elif control_message.type == "client_get_id":
+ await websocket_manager.direct_message(
+ target=socket_id,
+ sender=socket_id,
+ type=control_message.type,
+ data={"id": socket_id},
+ )
+
+
+async def client_message_router(websocket: WebSocket, raw_message: Any):
+ """Route incoming websocket messages to appropriate handlers."""
+ try:
+ message = ClientMessage.model_validate(raw_message)
+ except Exception:
+ logger.warning("Invalid websocket message received: %s", raw_message)
+ return
+
+ socket_id = get_state(websocket, "socket_id", "")
+ logger.info("Socket %s sent message %s", socket_id, message.model_dump())
+
+ if message.route == "broadcast":
+ return await websocket_manager.broadcast(
+ sender=socket_id, type=message.type, data=message.data
+ )
+ elif message.route == "room":
+ return await websocket_manager.room_message(
+ target=message.target,
+ sender=socket_id,
+ type=message.type,
+ data=message.data,
+ )
+ elif message.route == "client":
+ return await websocket_manager.direct_message(
+ target=message.target,
+ sender=socket_id,
+ type=message.type,
+ data=message.data,
+ )
+ elif message.route == "control":
+ return await client_controls(websocket=websocket, message=message)
+
+
+# Register the message router
+websocket_manager.on(CLIENT_MESSAGE_EVENT, client_message_router)
+''',
+ # Client message schemas
+ "schemas/client_message.py": '''"""
+Websocket client message schemas for {{PROJECT_NAME}}
+"""
+from typing import Any
+from fastapi_cruddy_framework import CruddyGenericModel
+
+
+class ClientMessage(CruddyGenericModel):
+ """Base websocket message from client."""
+ route: str # "broadcast", "room", "client", "control"
+ type: str # Message type identifier
+ target: str | None = None # Target room/client (if applicable)
+ data: Any = None # Message payload
+
+
+class ClientControlWithTarget(CruddyGenericModel):
+ """Control message with required target field."""
+ type: str # Control command type
+ target: str # Target ID for control operation
+ data: Any = None
+''',
+ # Middleware
+ "middleware/__init__.py": '''"""
+Middleware modules for {{PROJECT_NAME}}
+"""
+''',
+ "middleware/request_logger.py": '''"""
+Request logging middleware for {{PROJECT_NAME}}
+"""
+import logging
+from random import choices
+from time import time
+from string import ascii_uppercase, digits
+from fastapi import Request
+from starlette.middleware.base import BaseHTTPMiddleware
+
+logger = logging.getLogger(__name__)
+
+
+class RequestLogger(BaseHTTPMiddleware):
+ """Middleware to log HTTP requests with timing and unique identifiers."""
+
+ async def dispatch(self, request: Request, call_next):
+ # Generate unique request ID
+ req_id = "".join(choices(ascii_uppercase + digits, k=6))
+ logger.info(f"rid={req_id} start request path={request.url.path}")
+
+ start_time = time()
+ response = await call_next(request)
+ process_time = (time() - start_time) * 1000
+ formatted_process_time = "{0:.2f}".format(process_time)
+
+ logger.info(
+ f"rid={req_id} completed_in={formatted_process_time}ms status_code={response.status_code}"
+ )
+
+ return response
+''',
+ # Enhanced pyproject.toml with additional dependencies
+ "pyproject.toml": """[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "{{PROJECT_NAME_LOWER}}"
+version = "0.1.0"
+description = "A FastAPI Cruddy Framework application with Auth, Websockets, and GraphQL"
+authors = ["Your Name "]
+readme = "README.md"
+packages = [{include = "{{PROJECT_NAME_LOWER}}"}]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+fastapi = {extras = ["all"], version = "^0.115.13"}
+fastapi-cruddy-framework = "^1.10.0"
+uvicorn = {extras = ["standard"], version = "^0.32.0"}
+pydantic-settings = "^2.0.0"
+aiosqlite = "^0.20.0"
+starlette-session = "^0.4.3"
+redis = "^5.2.0"
+strawberry-graphql = "^0.266.0"
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.0.0"
+pytest-asyncio = "^0.23.0"
+black = "^24.0.0"
+ruff = "^0.1.0"
+fakeredis = "^2.28.0"
+
+[tool.poetry.scripts]
+start = "{{PROJECT_NAME_LOWER}}.bootloader:start"
+
+[tool.black]
+line-length = 88
+target-version = ['py310']
+
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+select = ["E", "W", "F", "I"]
+ignore = ["E501"]
+""",
+ # Enhanced README with full features
+ "README.md": """# {{PROJECT_NAME}}
+
+A full-featured FastAPI Cruddy Framework application with authentication, websockets, GraphQL, and more.
+
+## Features
+
+- 🔐 **Authentication**: Bearer token and session-based auth
+- 🔌 **WebSockets**: Real-time messaging with rooms and broadcasting
+- 📊 **GraphQL**: GraphQL endpoint for flexible data queries
+- 🛡️ **Middleware**: Request logging and error handling
+- 📝 **CRUD Operations**: Auto-generated REST endpoints
+- 🔄 **Modern Stack**: Poetry, async/await, type hints
+
+## Setup
+
+1. Install dependencies using Poetry:
+ ```bash
+ poetry install
+ ```
+
+2. Run the application:
+ ```bash
+ poetry run start
+ # OR
+ poetry run python main.py
+ ```
+
+3. Access the services:
+ - **API Documentation**: http://localhost:8000/docs
+ - **Health Check**: http://localhost:8000/health
+ - **WebSocket**: ws://localhost:8000/ws (requires auth)
+
+## Authentication
+
+The application uses bearer token authentication:
+
+```bash
+# Via Authorization header
+curl -H "Authorization: Bearer your-token" http://localhost:8000/health
+
+# Via query parameter
+curl http://localhost:8000/health?auth_token=your-token
+```
+
+## WebSocket Usage
+
+Connect to the WebSocket endpoint with authentication:
+
+```javascript
+const ws = new WebSocket('ws://localhost:8000/ws?auth_token=your-token');
+
+// Send a broadcast message
+ws.send(JSON.stringify({
+ route: "broadcast",
+ type: "chat_message",
+ data: { message: "Hello everyone!" }
+}));
+
+// Join a room
+ws.send(JSON.stringify({
+ route: "control",
+ type: "client_join_room",
+ target: "room_1"
+}));
+
+// Send message to room
+ws.send(JSON.stringify({
+ route: "room",
+ type: "room_message",
+ target: "room_1",
+ data: { message: "Hello room!" }
+}));
+```
+
+## Resource Generation
+
+### Generate a new resource:
+```bash
+cruddy generate resource User --fields "name:str,email:str,age:int"
+```
+
+### Generate individual components:
+```bash
+cruddy generate model Product --fields "name:str,price:float,description:str"
+cruddy generate controller CustomEndpoints
+```
+
+## Development
+
+### Code formatting:
+```bash
+poetry run black .
+```
+
+### Linting:
+```bash
+poetry run ruff check .
+```
+
+### Tests:
+```bash
+poetry run pytest
+```
+
+## Project Structure
+
+- `main.py` - FastAPI application with full middleware stack
+- `router/application.py` - Main router with websocket endpoints
+- `policies/` - Authentication and authorization policies
+- `services/` - WebSocket managers and business logic
+- `middleware/` - Request logging and custom middleware
+- `schemas/` - Pydantic models for validation
+- `models/` - Database models
+- `resources/` - Resource definitions (CRUD endpoints)
+- `controllers/` - Custom controller extensions
+- `config/` - Configuration settings
+
+## Production Notes
+
+For production deployment:
+- Change `redis_mode` from "memory" to "connection" in websocket manager
+- Set up proper Redis server
+- Configure environment variables in `.env`
+- Use proper authentication tokens
+- Enable HTTPS for websocket connections
+""",
+}
+
+FULL_POSTGRESQL_TEMPLATE = {
+ **FULL_SQLITE_TEMPLATE,
+ "adapters/__init__.py": MINIMAL_POSTGRESQL_TEMPLATE["adapters/__init__.py"],
+ "adapters/application.py": MINIMAL_POSTGRESQL_TEMPLATE["adapters/application.py"],
+ "pyproject.toml": """[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "{{PROJECT_NAME_LOWER}}"
+version = "0.1.0"
+description = "A FastAPI Cruddy Framework application with Auth, Websockets, and GraphQL"
+authors = ["Your Name "]
+readme = "README.md"
+packages = [{include = "{{PROJECT_NAME_LOWER}}"}]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+fastapi = {extras = ["all"], version = "^0.115.13"}
+fastapi-cruddy-framework = "^1.10.0"
+uvicorn = {extras = ["standard"], version = "^0.32.0"}
+pydantic-settings = "^2.0.0"
+asyncpg = "^0.30.0"
+starlette-session = "^0.4.3"
+redis = "^5.2.0"
+strawberry-graphql = "^0.266.0"
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.0.0"
+pytest-asyncio = "^0.23.0"
+black = "^24.0.0"
+ruff = "^0.1.0"
+fakeredis = "^2.28.0"
+
+[tool.poetry.scripts]
+start = "{{PROJECT_NAME_LOWER}}.bootloader:start"
+
+[tool.black]
+line-length = 88
+target-version = ['py310']
+
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+select = ["E", "W", "F", "I"]
+ignore = ["E501"]
+""",
+ ".env": MINIMAL_POSTGRESQL_TEMPLATE[".env"],
+}
+
+FULL_MYSQL_TEMPLATE = {
+ **FULL_SQLITE_TEMPLATE,
+ "adapters/__init__.py": MINIMAL_MYSQL_TEMPLATE["adapters/__init__.py"],
+ "adapters/application.py": MINIMAL_MYSQL_TEMPLATE["adapters/application.py"],
+ "pyproject.toml": """[build-system]
+requires = ["poetry-core>=1.0.0"]
+build-backend = "poetry.core.masonry.api"
+
+[tool.poetry]
+name = "{{PROJECT_NAME_LOWER}}"
+version = "0.1.0"
+description = "A FastAPI Cruddy Framework application with Auth, Websockets, and GraphQL"
+authors = ["Your Name "]
+readme = "README.md"
+packages = [{include = "{{PROJECT_NAME_LOWER}}"}]
+
+[tool.poetry.dependencies]
+python = "^3.10"
+fastapi = {extras = ["all"], version = "^0.115.13"}
+fastapi-cruddy-framework = "^1.10.0"
+uvicorn = {extras = ["standard"], version = "^0.32.0"}
+pydantic-settings = "^2.0.0"
+PyMySQL = "^1.0.0"
+starlette-session = "^0.4.3"
+redis = "^5.2.0"
+strawberry-graphql = "^0.266.0"
+
+[tool.poetry.group.dev.dependencies]
+pytest = "^8.0.0"
+pytest-asyncio = "^0.23.0"
+black = "^24.0.0"
+ruff = "^0.1.0"
+fakeredis = "^2.28.0"
+
+[tool.poetry.scripts]
+start = "{{PROJECT_NAME_LOWER}}.bootloader:start"
+
+[tool.black]
+line-length = 88
+target-version = ['py310']
+
+[tool.ruff]
+target-version = "py310"
+line-length = 88
+select = ["E", "W", "F", "I"]
+ignore = ["E501"]
+""",
+ ".env": MINIMAL_MYSQL_TEMPLATE[".env"],
+}
+
+# Organize templates by template type and database
+PROJECT_TEMPLATES = {
+ "minimal": {
+ "sqlite": MINIMAL_SQLITE_TEMPLATE,
+ "postgresql": MINIMAL_POSTGRESQL_TEMPLATE,
+ "mysql": MINIMAL_MYSQL_TEMPLATE,
+ },
+ "full": {
+ "sqlite": FULL_SQLITE_TEMPLATE,
+ "postgresql": FULL_POSTGRESQL_TEMPLATE,
+ "mysql": FULL_MYSQL_TEMPLATE,
+ },
+}
diff --git a/poetry.lock b/poetry.lock
index ed03eae..a650eca 100644
--- a/poetry.lock
+++ b/poetry.lock
@@ -1,4 +1,4 @@
-# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
+# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand.
[[package]]
name = "aiosqlite"
@@ -6,6 +6,7 @@ version = "0.20.0"
description = "asyncio bridge to the standard sqlite3 module"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "aiosqlite-0.20.0-py3-none-any.whl", hash = "sha256:36a1deaca0cac40ebe32aac9977a6e2bbc7f5189f23f4a54d5908986729e5bd6"},
{file = "aiosqlite-0.20.0.tar.gz", hash = "sha256:6d35c8c256637f4672f843c31021464090805bf925385ac39473fb16eaaca3d7"},
@@ -24,6 +25,7 @@ version = "0.7.0"
description = "Reusable constraint types to use with typing.Annotated"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"},
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
@@ -35,6 +37,7 @@ version = "4.9.0"
description = "High level compatibility layer for multiple asynchronous event loop implementations"
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c"},
{file = "anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028"},
@@ -48,7 +51,7 @@ typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
[package.extras]
doc = ["Sphinx (>=8.2,<9.0)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx_rtd_theme"]
-test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1)", "uvloop (>=0.21)"]
+test = ["anyio[trio]", "blockbuster (>=1.5.23)", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "trustme", "truststore (>=0.9.1) ; python_version >= \"3.10\"", "uvloop (>=0.21) ; platform_python_implementation == \"CPython\" and platform_system != \"Windows\" and python_version < \"3.14\""]
trio = ["trio (>=0.26.1)"]
[[package]]
@@ -57,6 +60,7 @@ version = "3.3.9"
description = "An abstract syntax tree for Python with inference support."
optional = false
python-versions = ">=3.9.0"
+groups = ["dev"]
files = [
{file = "astroid-3.3.9-py3-none-any.whl", hash = "sha256:d05bfd0acba96a7bd43e222828b7d9bc1e138aaeb0649707908d3702a9831248"},
{file = "astroid-3.3.9.tar.gz", hash = "sha256:622cc8e3048684aa42c820d9d218978021c3c3d174fb03a9f0d615921744f550"},
@@ -71,6 +75,7 @@ version = "1.4.11"
description = "Async client for testing ASGI web applications"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "async-asgi-testclient-1.4.11.tar.gz", hash = "sha256:4449ac85d512d661998ec61f91c9ae01851639611d748d81ae7f816736551792"},
]
@@ -85,6 +90,7 @@ version = "4.0.3"
description = "Timeout context manager for asyncio programs"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"},
{file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"},
@@ -96,18 +102,19 @@ version = "25.3.0"
description = "Classes Without Boilerplate"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"},
{file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"},
]
[package.extras]
-benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
+benchmark = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
+cov = ["cloudpickle ; platform_python_implementation == \"CPython\"", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
+dev = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"]
-tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
-tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
+tests = ["cloudpickle ; platform_python_implementation == \"CPython\"", "hypothesis", "mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-xdist[psutil]"]
+tests-mypy = ["mypy (>=1.11.1) ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\"", "pytest-mypy-plugins ; platform_python_implementation == \"CPython\" and python_version >= \"3.10\""]
[[package]]
name = "bcrypt"
@@ -115,6 +122,7 @@ version = "4.3.0"
description = "Modern password hashing for your software and your servers"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "bcrypt-4.3.0-cp313-cp313t-macosx_10_12_universal2.whl", hash = "sha256:f01e060f14b6b57bbb72fc5b4a83ac21c443c9a2ee708e04a10e9192f90a6281"},
{file = "bcrypt-4.3.0-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5eeac541cefd0bb887a371ef73c62c3cd78535e4887b310626036a7c0a817bb"},
@@ -179,6 +187,7 @@ version = "24.10.0"
description = "The uncompromising code formatter."
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "black-24.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e6668650ea4b685440857138e5fe40cde4d652633b1bdffc62933d0db4ed9812"},
{file = "black-24.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:1c536fcf674217e87b8cc3657b81809d3c085d7bf3ef262ead700da345bfa6ea"},
@@ -225,6 +234,7 @@ version = "2025.1.31"
description = "Python package for providing Mozilla's CA Bundle."
optional = false
python-versions = ">=3.6"
+groups = ["main"]
files = [
{file = "certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe"},
{file = "certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651"},
@@ -236,6 +246,7 @@ version = "3.4.1"
description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "charset_normalizer-3.4.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:91b36a978b5ae0ee86c394f5a54d6ef44db1de0815eb43de826d41d21e4af3de"},
{file = "charset_normalizer-3.4.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7461baadb4dc00fd9e0acbe254e3d7d2112e7f92ced2adc96e54ef6501c5f176"},
@@ -337,6 +348,7 @@ version = "8.1.8"
description = "Composable command line interface toolkit"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
files = [
{file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"},
{file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"},
@@ -351,6 +363,8 @@ version = "0.4.6"
description = "Cross-platform colored terminal text."
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
+groups = ["main", "dev"]
+markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
files = [
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
@@ -362,6 +376,7 @@ version = "7.8.0"
description = "Code coverage measurement for Python"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "coverage-7.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2931f66991175369859b5fd58529cd4b73582461877ecfd859b6549869287ffe"},
{file = "coverage-7.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:52a523153c568d2c0ef8826f6cc23031dc86cffb8c6aeab92c4ff776e7951b28"},
@@ -429,7 +444,7 @@ files = [
]
[package.extras]
-toml = ["tomli"]
+toml = ["tomli ; python_full_version <= \"3.11.0a6\""]
[[package]]
name = "dill"
@@ -437,6 +452,7 @@ version = "0.4.0"
description = "serialize all of Python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "dill-0.4.0-py3-none-any.whl", hash = "sha256:44f54bf6412c2c8464c14e8243eb163690a9800dbe2c367330883b19c7561049"},
{file = "dill-0.4.0.tar.gz", hash = "sha256:0633f1d2df477324f53a895b02c901fb961bdbf65a17122586ea7019292cbcf0"},
@@ -452,6 +468,7 @@ version = "2.7.0"
description = "DNS toolkit"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "dnspython-2.7.0-py3-none-any.whl", hash = "sha256:b4c34b7d10b51bcc3a5071e7b8dee77939f1e878477eeecc965e9835f63c6c86"},
{file = "dnspython-2.7.0.tar.gz", hash = "sha256:ce9c432eda0dc91cf618a5cedf1a4e142651196bbcd2c80e89ed5a907e5cfaf1"},
@@ -472,6 +489,7 @@ version = "2.2.0"
description = "A robust email address syntax and deliverability validation library."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"},
{file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"},
@@ -487,6 +505,8 @@ version = "1.2.2"
description = "Backport of PEP 654 (exception groups)"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
+markers = "python_version == \"3.10\""
files = [
{file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"},
{file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"},
@@ -501,6 +521,7 @@ version = "2.28.1"
description = "Python implementation of redis API, can be used for testing purposes."
optional = false
python-versions = "<4.0,>=3.7"
+groups = ["main"]
files = [
{file = "fakeredis-2.28.1-py3-none-any.whl", hash = "sha256:38c7c17fba5d5522af9d980a8f74a4da9900a3441e8f25c0fe93ea4205d695d1"},
{file = "fakeredis-2.28.1.tar.gz", hash = "sha256:5e542200b945aa0a7afdc0396efefe3cdabab61bc0f41736cc45f68960255964"},
@@ -524,6 +545,7 @@ version = "0.115.12"
description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "fastapi-0.115.12-py3-none-any.whl", hash = "sha256:e94613d6c05e27be7ffebdd6ea5f388112e5e430c8f7d6494a9d1d88d43e814d"},
{file = "fastapi-0.115.12.tar.gz", hash = "sha256:1e2c2a2646905f9e83d32f04a3f86aff4a286669c6c950ca95b5fd68c2602681"},
@@ -556,6 +578,7 @@ version = "0.0.7"
description = "Run and manage FastAPI apps from the command line with FastAPI CLI. 🚀"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "fastapi_cli-0.0.7-py3-none-any.whl", hash = "sha256:d549368ff584b2804336c61f192d86ddea080c11255f375959627911944804f4"},
{file = "fastapi_cli-0.0.7.tar.gz", hash = "sha256:02b3b65956f526412515907a0793c9094abd4bfb5457b389f645b0ea6ba3605e"},
@@ -575,6 +598,7 @@ version = "3.2.6"
description = "GraphQL implementation for Python, a port of GraphQL.js, the JavaScript reference implementation for GraphQL."
optional = false
python-versions = "<4,>=3.6"
+groups = ["main"]
files = [
{file = "graphql_core-3.2.6-py3-none-any.whl", hash = "sha256:78b016718c161a6fb20a7d97bbf107f331cd1afe53e45566c59f776ed7f0b45f"},
{file = "graphql_core-3.2.6.tar.gz", hash = "sha256:c08eec22f9e40f0bd61d805907e3b3b1b9a320bc606e23dc145eebca07c8fbab"},
@@ -586,6 +610,8 @@ version = "3.2.1"
description = "Lightweight in-process concurrent programming"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
+markers = "python_version < \"3.14\" and (platform_machine == \"aarch64\" or platform_machine == \"ppc64le\" or platform_machine == \"x86_64\" or platform_machine == \"amd64\" or platform_machine == \"AMD64\" or platform_machine == \"win32\" or platform_machine == \"WIN32\")"
files = [
{file = "greenlet-3.2.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:777c1281aa7c786738683e302db0f55eb4b0077c20f1dc53db8852ffaea0a6b0"},
{file = "greenlet-3.2.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3059c6f286b53ea4711745146ffe5a5c5ff801f62f6c56949446e0f6461f8157"},
@@ -654,6 +680,7 @@ version = "0.14.0"
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
files = [
{file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"},
{file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"},
@@ -665,6 +692,7 @@ version = "1.0.8"
description = "A minimal low-level HTTP client."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "httpcore-1.0.8-py3-none-any.whl", hash = "sha256:5254cf149bcb5f75e9d1b2b9f729ea4a4b883d1ad7379fc632b727cec23674be"},
{file = "httpcore-1.0.8.tar.gz", hash = "sha256:86e94505ed24ea06514883fd44d2bc02d90e77e7979c8eb71b90f41d364a1bad"},
@@ -686,6 +714,7 @@ version = "0.6.4"
description = "A collection of framework independent HTTP protocol utils."
optional = false
python-versions = ">=3.8.0"
+groups = ["main"]
files = [
{file = "httptools-0.6.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:3c73ce323711a6ffb0d247dcd5a550b8babf0f757e86a52558fe5b86d6fefcc0"},
{file = "httptools-0.6.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:345c288418f0944a6fe67be8e6afa9262b18c7626c3ef3c28adc5eabc06a68da"},
@@ -741,6 +770,7 @@ version = "0.28.1"
description = "The next generation HTTP client."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
@@ -753,7 +783,7 @@ httpcore = "==1.*"
idna = "*"
[package.extras]
-brotli = ["brotli", "brotlicffi"]
+brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
http2 = ["h2 (>=3,<5)"]
socks = ["socksio (==1.*)"]
@@ -765,6 +795,7 @@ version = "3.10"
description = "Internationalized Domain Names in Applications (IDNA)"
optional = false
python-versions = ">=3.6"
+groups = ["main", "dev"]
files = [
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
@@ -779,6 +810,7 @@ version = "7.5.0"
description = "Correctly generate plurals, singular nouns, ordinals, indefinite articles"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "inflect-7.5.0-py3-none-any.whl", hash = "sha256:2aea70e5e70c35d8350b8097396ec155ffd68def678c7ff97f51aa69c1d92344"},
{file = "inflect-7.5.0.tar.gz", hash = "sha256:faf19801c3742ed5a05a8ce388e0d8fe1a07f8d095c82201eb904f5d27ad571f"},
@@ -789,7 +821,7 @@ more_itertools = ">=8.5.0"
typeguard = ">=4.0.1"
[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"]
enabler = ["pytest-enabler (>=2.2)"]
@@ -802,6 +834,7 @@ version = "2.1.0"
description = "brain-dead simple config-ini parsing"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760"},
{file = "iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7"},
@@ -813,6 +846,7 @@ version = "6.0.1"
description = "A Python utility / library to sort Python imports."
optional = false
python-versions = ">=3.9.0"
+groups = ["dev"]
files = [
{file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"},
{file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"},
@@ -828,6 +862,7 @@ version = "2.2.0"
description = "Safely pass data to untrusted environments and back."
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "itsdangerous-2.2.0-py3-none-any.whl", hash = "sha256:c6242fc49e35958c8b15141343aa660db5fc54d4f13a1db01a3f5891b98700ef"},
{file = "itsdangerous-2.2.0.tar.gz", hash = "sha256:e0050c0b7da1eea53ffaf149c0cfbb5c6e2e2b69c4bef22c81fa6eb73e5f6173"},
@@ -839,6 +874,7 @@ version = "3.1.6"
description = "A very fast and expressive template engine."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
@@ -856,6 +892,7 @@ version = "4.23.0"
description = "An implementation of JSON Schema validation for Python"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"},
{file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"},
@@ -877,6 +914,7 @@ version = "2025.4.1"
description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af"},
{file = "jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608"},
@@ -891,6 +929,7 @@ version = "3.0.0"
description = "Python port of markdown-it. Markdown parsing, done right!"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb"},
{file = "markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1"},
@@ -915,6 +954,7 @@ version = "3.0.2"
description = "Safely add untrusted strings to HTML/XML markup."
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
@@ -985,6 +1025,7 @@ version = "0.7.0"
description = "McCabe checker, plugin for flake8"
optional = false
python-versions = ">=3.6"
+groups = ["dev"]
files = [
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
@@ -996,6 +1037,7 @@ version = "0.1.2"
description = "Markdown URL utilities"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"},
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
@@ -1007,6 +1049,7 @@ version = "10.7.0"
description = "More routines for operating on iterables, beyond itertools"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e"},
{file = "more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3"},
@@ -1018,6 +1061,7 @@ version = "6.4.3"
description = "multidict implementation"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "multidict-6.4.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:32a998bd8a64ca48616eac5a8c1cc4fa38fb244a3facf2eeb14abe186e0f6cc5"},
{file = "multidict-6.4.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:a54ec568f1fc7f3c313c2f3b16e5db346bf3660e1309746e7fccbbfded856188"},
@@ -1134,6 +1178,7 @@ version = "1.1.0"
description = "Type system extensions for programs checked with the mypy type checker."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
@@ -1145,6 +1190,7 @@ version = "3.10.16"
description = "Fast, correct Python JSON library supporting dataclasses, datetimes, and numpy"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "orjson-3.10.16-cp310-cp310-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:4cb473b8e79154fa778fb56d2d73763d977be3dcc140587e07dbc545bbfc38f8"},
{file = "orjson-3.10.16-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:622a8e85eeec1948690409a19ca1c7d9fd8ff116f4861d261e6ae2094fe59a00"},
@@ -1222,6 +1268,7 @@ version = "25.0"
description = "Core utilities for Python packages"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
@@ -1233,6 +1280,7 @@ version = "0.12.1"
description = "Utility library for gitignore style pattern matching of file paths."
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
@@ -1244,6 +1292,7 @@ version = "4.3.7"
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "platformdirs-4.3.7-py3-none-any.whl", hash = "sha256:a03875334331946f13c549dbd8f4bac7a13a50a895a0eb1e8c6a8ace80d40a94"},
{file = "platformdirs-4.3.7.tar.gz", hash = "sha256:eb437d586b6a0986388f0d6f74aa0cde27b48d0e3d66843640bfb6bdcdb6e351"},
@@ -1260,6 +1309,7 @@ version = "1.5.0"
description = "plugin and hook calling mechanisms for python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"},
{file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"},
@@ -1275,6 +1325,7 @@ version = "2.11.3"
description = "Data validation using Python type hints"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f"},
{file = "pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3"},
@@ -1288,7 +1339,7 @@ typing-inspection = ">=0.4.0"
[package.extras]
email = ["email-validator (>=2.0.0)"]
-timezone = ["tzdata"]
+timezone = ["tzdata ; python_version >= \"3.9\" and platform_system == \"Windows\""]
[[package]]
name = "pydantic-core"
@@ -1296,6 +1347,7 @@ version = "2.33.1"
description = "Core functionality for Pydantic validation and serialization"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3077cfdb6125cc8dab61b155fdd714663e401f0e6883f9632118ec12cf42df26"},
{file = "pydantic_core-2.33.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8ffab8b2908d152e74862d276cf5017c81a2f3719f14e8e3e8d6b83fda863927"},
@@ -1407,6 +1459,7 @@ version = "2.10.3"
description = "Extra Pydantic types."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pydantic_extra_types-2.10.3-py3-none-any.whl", hash = "sha256:e8b372752b49019cd8249cc192c62a820d8019f5382a8789d0f887338a59c0f3"},
{file = "pydantic_extra_types-2.10.3.tar.gz", hash = "sha256:dcc0a7b90ac9ef1b58876c9b8fdede17fbdde15420de9d571a9fccde2ae175bb"},
@@ -1417,11 +1470,11 @@ pydantic = ">=2.5.2"
typing-extensions = "*"
[package.extras]
-all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "pymongo (>=4.0.0,<5.0.0)", "python-ulid (>=1,<2)", "python-ulid (>=1,<4)", "pytz (>=2024.1)", "semver (>=3.0.2)", "semver (>=3.0.2,<3.1.0)", "tzdata (>=2024.1)"]
+all = ["pendulum (>=3.0.0,<4.0.0)", "phonenumbers (>=8,<9)", "pycountry (>=23)", "pymongo (>=4.0.0,<5.0.0)", "python-ulid (>=1,<2) ; python_version < \"3.9\"", "python-ulid (>=1,<4) ; python_version >= \"3.9\"", "pytz (>=2024.1)", "semver (>=3.0.2)", "semver (>=3.0.2,<3.1.0)", "tzdata (>=2024.1)"]
pendulum = ["pendulum (>=3.0.0,<4.0.0)"]
phonenumbers = ["phonenumbers (>=8,<9)"]
pycountry = ["pycountry (>=23)"]
-python-ulid = ["python-ulid (>=1,<2)", "python-ulid (>=1,<4)"]
+python-ulid = ["python-ulid (>=1,<2) ; python_version < \"3.9\"", "python-ulid (>=1,<4) ; python_version >= \"3.9\""]
semver = ["semver (>=3.0.2)"]
[[package]]
@@ -1430,6 +1483,7 @@ version = "2.9.1"
description = "Settings management using Pydantic"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "pydantic_settings-2.9.1-py3-none-any.whl", hash = "sha256:59b4f431b1defb26fe620c71a7d3968a710d719f5f4cdbbdb7926edeb770f6ef"},
{file = "pydantic_settings-2.9.1.tar.gz", hash = "sha256:c509bf79d27563add44e8446233359004ed85066cd096d8b510f715e6ef5d268"},
@@ -1453,6 +1507,7 @@ version = "2.19.1"
description = "Pygments is a syntax highlighting package written in Python."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c"},
{file = "pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f"},
@@ -1467,6 +1522,7 @@ version = "3.3.6"
description = "python code static checker"
optional = false
python-versions = ">=3.9.0"
+groups = ["dev"]
files = [
{file = "pylint-3.3.6-py3-none-any.whl", hash = "sha256:8b7c2d3e86ae3f94fb27703d521dd0b9b6b378775991f504d7c3a6275aa0a6a6"},
{file = "pylint-3.3.6.tar.gz", hash = "sha256:b634a041aac33706d56a0d217e6587228c66427e20ec21a019bc4cdee48c040a"},
@@ -1478,7 +1534,7 @@ colorama = {version = ">=0.4.5", markers = "sys_platform == \"win32\""}
dill = [
{version = ">=0.2", markers = "python_version < \"3.11\""},
{version = ">=0.3.7", markers = "python_version >= \"3.12\""},
- {version = ">=0.3.6", markers = "python_version >= \"3.11\" and python_version < \"3.12\""},
+ {version = ">=0.3.6", markers = "python_version == \"3.11\""},
]
isort = ">=4.2.5,<5.13 || >5.13,<7"
mccabe = ">=0.6,<0.8"
@@ -1496,13 +1552,14 @@ version = "1.0.0"
description = "Python port of the extended Node.js EventEmitter 2 approach providing namespaces, wildcards and TTL."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "pymitter-1.0.0-py3-none-any.whl", hash = "sha256:4c540a76e913e1399218cbf9bcd4d43a48ca1b619a52203c37847d1a06d9864c"},
{file = "pymitter-1.0.0.tar.gz", hash = "sha256:0ee8450d81079736db0825b71c1fe7ad5a2bf1be681cabaaaeb1b0a920a0e7ec"},
]
[package.extras]
-dev = ["flake8 (>=5.0.0,<5.1.0)", "flake8 (>=7.0.0,<7.1.0)", "flake8-commas (>=2.1.0,<2.2.0)", "flake8-quotes (>=3.3.2,<3.4.0)", "mypy (>=1.4.1)", "pytest-cov (>=3.0)", "types-docutils (>=0.20.0,<0.21.0)", "typing-extensions (>=4.7.1)"]
+dev = ["flake8 (>=5.0.0,<5.1.0) ; python_version < \"3.8\"", "flake8 (>=7.0.0,<7.1.0) ; python_version >= \"3.8\"", "flake8-commas (>=2.1.0,<2.2.0)", "flake8-quotes (>=3.3.2,<3.4.0)", "mypy (>=1.4.1)", "pytest-cov (>=3.0)", "types-docutils (>=0.20.0,<0.21.0)", "typing-extensions (>=4.7.1)"]
[[package]]
name = "pytest"
@@ -1510,6 +1567,7 @@ version = "8.3.5"
description = "pytest: simple powerful testing with Python"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "pytest-8.3.5-py3-none-any.whl", hash = "sha256:c69214aa47deac29fad6c2a4f590b9c4a9fdb16a403176fe154b79c0b4d4d820"},
{file = "pytest-8.3.5.tar.gz", hash = "sha256:f4efe70cc14e511565ac476b57c279e12a855b11f48f212af1080ef2263d3845"},
@@ -1532,6 +1590,7 @@ version = "0.6.0"
description = "Manage dependencies of tests"
optional = false
python-versions = ">=3.4"
+groups = ["dev"]
files = [
{file = "pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1"},
]
@@ -1546,6 +1605,7 @@ version = "2.9.0.post0"
description = "Extensions to the standard Python datetime module"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main"]
files = [
{file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"},
{file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"},
@@ -1560,6 +1620,7 @@ version = "1.1.0"
description = "Read key-value pairs from a .env file and set them as environment variables"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "python_dotenv-1.1.0-py3-none-any.whl", hash = "sha256:d7c01d9e2293916c18baf562d95698754b0dbbb5e74d457c45d4f6561fb9d55d"},
{file = "python_dotenv-1.1.0.tar.gz", hash = "sha256:41f90bc6f5f177fb41f53e87666db362025010eb28f60a01c9143bfa33a2b2d5"},
@@ -1574,6 +1635,7 @@ version = "0.0.20"
description = "A streaming multipart parser for Python"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104"},
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
@@ -1585,6 +1647,7 @@ version = "6.0.2"
description = "YAML parser and emitter for Python"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"},
{file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"},
@@ -1647,6 +1710,7 @@ version = "5.2.1"
description = "Python client for Redis database and key-value store"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "redis-5.2.1-py3-none-any.whl", hash = "sha256:ee7e1056b9aea0f04c6c2ed59452947f34c4940ee025f5dd83e6a6418b6989e4"},
{file = "redis-5.2.1.tar.gz", hash = "sha256:16f2e22dff21d5125e8481515e386711a34cbec50f0e44413dd7d9c060a54e0f"},
@@ -1665,6 +1729,7 @@ version = "0.36.2"
description = "JSON Referencing + Python"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0"},
{file = "referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa"},
@@ -1681,6 +1746,7 @@ version = "2.32.3"
description = "Python HTTP for Humans."
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"},
{file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"},
@@ -1702,6 +1768,7 @@ version = "14.0.0"
description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal"
optional = false
python-versions = ">=3.8.0"
+groups = ["main"]
files = [
{file = "rich-14.0.0-py3-none-any.whl", hash = "sha256:1c9491e1951aac09caffd42f448ee3d04e58923ffe14993f6e83068dc395d7e0"},
{file = "rich-14.0.0.tar.gz", hash = "sha256:82f1bc23a6a21ebca4ae0c45af9bdbc492ed20231dcb63f297d6d1021a9d5725"},
@@ -1721,6 +1788,7 @@ version = "0.14.1"
description = "Rich toolkit for building command-line applications"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "rich_toolkit-0.14.1-py3-none-any.whl", hash = "sha256:dc92c0117d752446d04fdc828dbca5873bcded213a091a5d3742a2beec2e6559"},
{file = "rich_toolkit-0.14.1.tar.gz", hash = "sha256:9248e2d087bfc01f3e4c5c8987e05f7fa744d00dd22fa2be3aa6e50255790b3f"},
@@ -1737,6 +1805,7 @@ version = "0.24.0"
description = "Python bindings to Rust's persistent data structures (rpds)"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "rpds_py-0.24.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:006f4342fe729a368c6df36578d7a348c7c716be1da0a1a0f86e3021f8e98724"},
{file = "rpds_py-0.24.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2d53747da70a4e4b17f559569d5f9506420966083a31c5fbd84e764461c4444b"},
@@ -1860,19 +1929,20 @@ version = "79.0.0"
description = "Easily download, build, install, upgrade, and uninstall Python packages"
optional = false
python-versions = ">=3.9"
+groups = ["dev"]
files = [
{file = "setuptools-79.0.0-py3-none-any.whl", hash = "sha256:b9ab3a104bedb292323f53797b00864e10e434a3ab3906813a7169e4745b912a"},
{file = "setuptools-79.0.0.tar.gz", hash = "sha256:9828422e7541213b0aacb6e10bbf9dd8febeaa45a48570e09b6d100e063fc9f9"},
]
[package.extras]
-check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1)", "ruff (>=0.8.0)"]
-core = ["importlib_metadata (>=6)", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1)", "wheel (>=0.43.0)"]
+check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""]
+core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"]
cover = ["pytest-cov"]
doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
enabler = ["pytest-enabler (>=2.2)"]
-test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
-type = ["importlib_metadata (>=7.0.2)", "jaraco.develop (>=7.21)", "mypy (==1.14.*)", "pytest-mypy"]
+test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"]
+type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.14.*)", "pytest-mypy"]
[[package]]
name = "shellingham"
@@ -1880,6 +1950,7 @@ version = "1.5.4"
description = "Tool to Detect Surrounding Shell"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"},
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
@@ -1891,6 +1962,7 @@ version = "1.17.0"
description = "Python 2 and 3 compatibility utilities"
optional = false
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7"
+groups = ["main"]
files = [
{file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"},
{file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"},
@@ -1902,6 +1974,7 @@ version = "1.3.1"
description = "Sniff out which async library your code is running under"
optional = false
python-versions = ">=3.7"
+groups = ["main", "dev"]
files = [
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
@@ -1913,6 +1986,7 @@ version = "2.4.0"
description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set"
optional = false
python-versions = "*"
+groups = ["main"]
files = [
{file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"},
{file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"},
@@ -1924,6 +1998,7 @@ version = "2.0.40"
description = "Database Abstraction Library"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "SQLAlchemy-2.0.40-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:ae9597cab738e7cc823f04a704fb754a9249f0b6695a6aeb63b74055cd417a96"},
{file = "SQLAlchemy-2.0.40-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37a5c21ab099a83d669ebb251fddf8f5cee4d75ea40a5a1653d9c43d60e20867"},
@@ -2019,6 +2094,7 @@ version = "0.41.2"
description = "Various utility functions for SQLAlchemy."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "SQLAlchemy-Utils-0.41.2.tar.gz", hash = "sha256:bc599c8c3b3319e53ce6c5c3c471120bd325d0071fb6f38a10e924e3d07b9990"},
{file = "SQLAlchemy_Utils-0.41.2-py3-none-any.whl", hash = "sha256:85cf3842da2bf060760f955f8467b87983fb2e30f1764fd0e24a48307dc8ec6e"},
@@ -2036,8 +2112,8 @@ intervals = ["intervals (>=0.7.1)"]
password = ["passlib (>=1.6,<2.0)"]
pendulum = ["pendulum (>=2.0.5)"]
phone = ["phonenumbers (>=5.9.2)"]
-test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
-test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
+test = ["Jinja2 (>=2.3)", "Pygments (>=1.2)", "backports.zoneinfo ; python_version < \"3.9\"", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "isort (>=4.2.2)", "pg8000 (>=1.12.4)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
+test-all = ["Babel (>=1.3)", "Jinja2 (>=2.3)", "Pygments (>=1.2)", "arrow (>=0.3.4)", "backports.zoneinfo ; python_version < \"3.9\"", "colour (>=0.0.4)", "cryptography (>=0.6)", "docutils (>=0.10)", "flake8 (>=2.4.0)", "flexmock (>=0.9.7)", "furl (>=0.4.1)", "intervals (>=0.7.1)", "isort (>=4.2.2)", "passlib (>=1.6,<2.0)", "pendulum (>=2.0.5)", "pg8000 (>=1.12.4)", "phonenumbers (>=5.9.2)", "psycopg (>=3.1.8)", "psycopg2 (>=2.5.1)", "psycopg2cffi (>=2.8.1)", "pymysql", "pyodbc", "pytest (==7.4.4)", "python-dateutil", "python-dateutil (>=2.6)", "pytz (>=2014.2)"]
timezone = ["python-dateutil"]
url = ["furl (>=0.4.1)"]
@@ -2047,6 +2123,7 @@ version = "0.0.24"
description = "SQLModel, SQL databases in Python, designed for simplicity, compatibility, and robustness."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "sqlmodel-0.0.24-py3-none-any.whl", hash = "sha256:6778852f09370908985b667d6a3ab92910d0d5ec88adcaf23dbc242715ff7193"},
{file = "sqlmodel-0.0.24.tar.gz", hash = "sha256:cc5c7613c1a5533c9c7867e1aab2fd489a76c9e8a061984da11b4e613c182423"},
@@ -2062,6 +2139,7 @@ version = "0.46.2"
description = "The little ASGI library that shines."
optional = false
python-versions = ">=3.9"
+groups = ["main", "dev"]
files = [
{file = "starlette-0.46.2-py3-none-any.whl", hash = "sha256:595633ce89f8ffa71a015caed34a5b2dc1c0cdb3f0f1fbd1e69339cf2abeec35"},
{file = "starlette-0.46.2.tar.gz", hash = "sha256:7f7361f34eed179294600af672f565727419830b54b7b084efe44bb82d2fccd5"},
@@ -2079,6 +2157,7 @@ version = "0.4.3"
description = "A library for backend side session with starlette"
optional = false
python-versions = ">=3.7,<4"
+groups = ["dev"]
files = [
{file = "starlette_session-0.4.3-py3-none-any.whl", hash = "sha256:bfc2559ec0fec566b44865f20d1c4262b1132de867a936abe8771cf139cbbe0b"},
{file = "starlette_session-0.4.3.tar.gz", hash = "sha256:5b50a1c91a2ca1cf594574c0b386eea5dbed140dcb397ee73f0c43956bd2bfcc"},
@@ -2099,6 +2178,7 @@ version = "0.266.0"
description = "A library for creating GraphQL APIs"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "strawberry_graphql-0.266.0-py3-none-any.whl", hash = "sha256:d3ec2c96070aee33408148dfb3f34d676c2f263b68160d1bff2b32a41f053801"},
{file = "strawberry_graphql-0.266.0.tar.gz", hash = "sha256:93c221a1d19454b2d1303135733871a3a8b4f9a0cfeea3b7764cf615faf74b72"},
@@ -2123,7 +2203,7 @@ debug-server = ["libcst (>=0.4.7)", "pygments (>=2.3,<3.0)", "python-multipart (
django = ["Django (>=3.2)", "asgiref (>=3.2,<4.0)"]
fastapi = ["fastapi (>=0.65.2)", "python-multipart (>=0.0.7)"]
flask = ["flask (>=1.1)"]
-litestar = ["litestar (>=2)"]
+litestar = ["litestar (>=2) ; python_version >= \"3.10\" and python_version < \"4.0\""]
opentelemetry = ["opentelemetry-api (<2)", "opentelemetry-sdk (<2)"]
pydantic = ["pydantic (>1.6.1)"]
pyinstrument = ["pyinstrument (>=4.0.0)"]
@@ -2136,6 +2216,8 @@ version = "2.2.1"
description = "A lil' TOML parser"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
+markers = "python_version == \"3.10\""
files = [
{file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"},
{file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"},
@@ -2177,6 +2259,7 @@ version = "0.13.2"
description = "Style preserving TOML library"
optional = false
python-versions = ">=3.8"
+groups = ["dev"]
files = [
{file = "tomlkit-0.13.2-py3-none-any.whl", hash = "sha256:7a974427f6e119197f670fbbbeae7bef749a6c14e793db934baefc1b5f03efde"},
{file = "tomlkit-0.13.2.tar.gz", hash = "sha256:fff5fe59a87295b278abd31bec92c15d9bc4a06885ab12bcea52c71119392e79"},
@@ -2188,6 +2271,7 @@ version = "4.4.2"
description = "Run-time type checker for Python"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "typeguard-4.4.2-py3-none-any.whl", hash = "sha256:77a78f11f09777aeae7fa08585f33b5f4ef0e7335af40005b0c422ed398ff48c"},
{file = "typeguard-4.4.2.tar.gz", hash = "sha256:a6f1065813e32ef365bc3b3f503af8a96f9dd4e0033a02c28c4a4983de8c6c49"},
@@ -2198,7 +2282,7 @@ typing_extensions = ">=4.10.0"
[package.extras]
doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme (>=1.3.0)"]
-test = ["coverage[toml] (>=7)", "mypy (>=1.2.0)", "pytest (>=7)"]
+test = ["coverage[toml] (>=7)", "mypy (>=1.2.0) ; platform_python_implementation != \"PyPy\"", "pytest (>=7)"]
[[package]]
name = "typer"
@@ -2206,6 +2290,7 @@ version = "0.15.2"
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "typer-0.15.2-py3-none-any.whl", hash = "sha256:46a499c6107d645a9c13f7ee46c5d5096cae6f5fc57dd11eccbbb9ae3e44ddfc"},
{file = "typer-0.15.2.tar.gz", hash = "sha256:ab2fab47533a813c49fe1f16b1a370fd5819099c00b119e0633df65f22144ba5"},
@@ -2223,6 +2308,7 @@ version = "4.13.2"
description = "Backported and Experimental Type Hints for Python 3.8+"
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c"},
{file = "typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef"},
@@ -2234,6 +2320,7 @@ version = "0.4.0"
description = "Runtime typing introspection tools"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f"},
{file = "typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122"},
@@ -2248,6 +2335,7 @@ version = "5.10.0"
description = "Ultra fast JSON encoder and decoder for Python"
optional = false
python-versions = ">=3.8"
+groups = ["main"]
files = [
{file = "ujson-5.10.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2601aa9ecdbee1118a1c2065323bda35e2c5a2cf0797ef4522d485f9d3ef65bd"},
{file = "ujson-5.10.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:348898dd702fc1c4f1051bc3aacbf894caa0927fe2c53e68679c073375f732cf"},
@@ -2335,13 +2423,14 @@ version = "2.4.0"
description = "HTTP library with thread-safe connection pooling, file post, and more."
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813"},
{file = "urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466"},
]
[package.extras]
-brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"]
+brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""]
h2 = ["h2 (>=4,<5)"]
socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"]
zstd = ["zstandard (>=0.18.0)"]
@@ -2352,6 +2441,7 @@ version = "0.1.0"
description = "UUID version 7, generating time-sorted UUIDs with 200ns time resolution and 48 bits of randomness"
optional = false
python-versions = ">=3.7"
+groups = ["main"]
files = [
{file = "uuid7-0.1.0-py2.py3-none-any.whl", hash = "sha256:5e259bb63c8cb4aded5927ff41b444a80d0c7124e8a0ced7cf44efa1f5cccf61"},
{file = "uuid7-0.1.0.tar.gz", hash = "sha256:8c57aa32ee7456d3cc68c95c4530bc571646defac01895cfc73545449894a63c"},
@@ -2363,6 +2453,7 @@ version = "0.24.0.post1"
description = "The lightning-fast ASGI server."
optional = false
python-versions = ">=3.8"
+groups = ["main", "dev"]
files = [
{file = "uvicorn-0.24.0.post1-py3-none-any.whl", hash = "sha256:7c84fea70c619d4a710153482c0d230929af7bcf76c7bfa6de151f0a3a80121e"},
{file = "uvicorn-0.24.0.post1.tar.gz", hash = "sha256:09c8e5a79dc466bdf28dead50093957db184de356fcdc48697bad3bde4c2588e"},
@@ -2376,12 +2467,12 @@ httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standar
python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""}
typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""}
-uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
+uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\" and extra == \"standard\""}
watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""}
websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""}
[package.extras]
-standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"]
+standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"]
[[package]]
name = "uvloop"
@@ -2389,6 +2480,8 @@ version = "0.21.0"
description = "Fast implementation of asyncio event loop on top of libuv"
optional = false
python-versions = ">=3.8.0"
+groups = ["main"]
+markers = "sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\""
files = [
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:ec7e6b09a6fdded42403182ab6b832b71f4edaf7f37a9a0e371a01db5f0cb45f"},
{file = "uvloop-0.21.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:196274f2adb9689a289ad7d65700d37df0c0930fd8e4e743fa4834e850d7719d"},
@@ -2440,6 +2533,7 @@ version = "1.5.0"
description = "Collection of 60+ Python functions for validating data"
optional = false
python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, <4"
+groups = ["main"]
files = [
{file = "validator-collection-1.5.0.tar.gz", hash = "sha256:f9395cad9a30cb9864fa0c0d18a84daead1eb8807774f4e09f588f89b7dc77d8"},
{file = "validator_collection-1.5.0-py2.py3-none-any.whl", hash = "sha256:56f6dc65d86c2e5b8f04f22acedd33af2c233edc1082fe4d24fd3996bd19d0fe"},
@@ -2458,6 +2552,7 @@ version = "1.0.5"
description = "Simple, modern and high performance file watching and code reload in python."
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"},
{file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"},
@@ -2541,6 +2636,7 @@ version = "15.0.1"
description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)"
optional = false
python-versions = ">=3.9"
+groups = ["main"]
files = [
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d63efaa0cd96cf0c5fe4d581521d9fa87744540d4bc999ae6e08595a1014b45b"},
{file = "websockets-15.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ac60e3b188ec7574cb761b08d50fcedf9d77f1530352db4eef1707fe9dee7205"},
@@ -2614,6 +2710,6 @@ files = [
]
[metadata]
-lock-version = "2.0"
+lock-version = "2.1"
python-versions = ">=3.10,<4.0"
-content-hash = "677b4590541820716d9e49bf033d36d8dd66be5ddffb36a25f8237ed00d4397f"
+content-hash = "550be86ffa00cf372ea55eef5d1fd2a4972183c03e45337db74acb5ac2956ad5"
diff --git a/pyproject.toml b/pyproject.toml
index a5dc8d5..0159812 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -50,6 +50,8 @@ fakeredis = ">=2.21.3"
async-timeout = ">=4.0.0,<5.0.0"
pymitter = ">=0.5.0"
strawberry-graphql = {extras = ["fastapi"], version = ">=0.223.0"}
+click = "^8.1.0"
+pydantic-settings = ">=2.0.0"
[tool.poetry.group.dev.dependencies]
black = "^24.3.0"
@@ -64,6 +66,7 @@ coverage = "^7.4.4"
[tool.poetry.scripts]
start_sqlite = "examples.fastapi_cruddy_sqlite.bootloader:start"
+cruddy = "fastapi_cruddy_framework.cli.main:main"
[build-system]
requires = ["poetry-core"]