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"]