diff --git a/example/database-app/.env.testing b/example/database-app/.env.testing index 9d347393..ae53c9e2 100644 --- a/example/database-app/.env.testing +++ b/example/database-app/.env.testing @@ -1,10 +1,10 @@ APP_NAME="Masonite Testing" APP_ENV=testing -DB_HOST=localhost -DB_DATABASE=postgres_testing -DB_USER=postgres -DB_PASSWORD=postgres -DB_PORT=5432 +DB_HOST=127.0.0.1 +DB_DATABASE=database_app_test +DB_USERNAME=app +DB_PASSWORD=secret +DB_PORT=3306 LOG_CHANNEL=syslog diff --git a/example/database-app/.gitignore b/example/database-app/.gitignore index 88ea2708..aa305283 100644 --- a/example/database-app/.gitignore +++ b/example/database-app/.gitignore @@ -1,2 +1,3 @@ .venv storage +.claude diff --git a/example/database-app/app/http/controllers/auth_controller.py b/example/database-app/app/http/controllers/auth_controller.py new file mode 100644 index 00000000..221bd3ce --- /dev/null +++ b/example/database-app/app/http/controllers/auth_controller.py @@ -0,0 +1,44 @@ +from fastapi import HTTPException +import hashlib + +from app.models.user import User +from app.models.profile import Profile +from app.http.schemas.auth import StudentRegistrationRequest, TeacherRegistrationRequest + +class AuthController: + @staticmethod + async def register_teacher(data: TeacherRegistrationRequest): + # Check if user exists + existing_user = await User.where("email", data.email).first() + if existing_user: + raise HTTPException(status_code=400, detail="Email already registered") + + # Hash password + hashed_password = hashlib.md5(data.password.encode()).hexdigest() + + # Create user + user = User() + user.name = data.name + user.email = data.email + user.password = hashed_password + user.role = "teacher" + await user.save() + + # Workaround for asyncpg insert bug in masoniteorm returning dict to primary key + actual_user_id = user.id.get("id") if isinstance(user.id, dict) else user.id + + # Create teacher profile + profile = Profile() + profile.user_id = actual_user_id + profile.country = data.country + profile.phone_number = data.phone_number + profile.headline = data.headline + profile.description = data.description + profile.video_url = data.video_url + profile.hourly_rate = data.hourly_rate + import json + profile.languages_spoken = json.dumps(data.languages_spoken) + profile.subjects = json.dumps(data.subjects) + await profile.save() + + return {"message": "Teacher registered successfully", "user_id": actual_user_id} diff --git a/example/database-app/app/http/schemas/auth.py b/example/database-app/app/http/schemas/auth.py new file mode 100644 index 00000000..88cb46e4 --- /dev/null +++ b/example/database-app/app/http/schemas/auth.py @@ -0,0 +1,16 @@ +from pydantic import BaseModel, EmailStr, Field + +class StudentRegistrationRequest(BaseModel): + name: str = Field(..., min_length=2, max_length=255) + email: EmailStr + password: str = Field(..., min_length=8) + +class TeacherRegistrationRequest(StudentRegistrationRequest): + country: str = Field(..., min_length=2) + phone_number: str + headline: str = Field(..., min_length=5, max_length=255) + description: str = Field(..., min_length=50) + video_url: str + hourly_rate: int = Field(..., gt=0) + languages_spoken: list[str] + subjects: list[str] diff --git a/example/database-app/app/models/__init__.py b/example/database-app/app/models/__init__.py index 73dafbd8..7dd387c7 100644 --- a/example/database-app/app/models/__init__.py +++ b/example/database-app/app/models/__init__.py @@ -1,5 +1,6 @@ from .user import User -from .post import Post -from .tag import Tag -from .media import Media -from .post_tag import PostTag +from .profile import Profile +from .lesson import Lesson +from .course import Course +from .category import Category +from .review import Review diff --git a/example/database-app/app/models/category.py b/example/database-app/app/models/category.py new file mode 100644 index 00000000..29655e70 --- /dev/null +++ b/example/database-app/app/models/category.py @@ -0,0 +1,18 @@ +from typing import TYPE_CHECKING + +from fastapi_startkit.masoniteorm.models import Model +from fastapi_startkit.masoniteorm.relationships import HasMany, HasManyThrough + +if TYPE_CHECKING: + from app.models.course import Course + from app.models.lesson import Lesson + + +class Category(Model): + __table__ = "categories" + + name: str + description: str | None + + courses = HasMany("Course") + lessons = HasManyThrough(["Lesson", "Course"], "category_id", "course_id") diff --git a/example/database-app/app/models/course.py b/example/database-app/app/models/course.py new file mode 100644 index 00000000..7b31a9a6 --- /dev/null +++ b/example/database-app/app/models/course.py @@ -0,0 +1,30 @@ +from typing import TYPE_CHECKING + +from fastapi_startkit.masoniteorm.models import Model +from fastapi_startkit.masoniteorm.relationships import BelongsTo, HasMany, BelongsToMany, MorphMany + +if TYPE_CHECKING: + from app.models.category import Category + from app.models.lesson import Lesson + from app.models.user import User + from app.models.review import Review + + +class Course(Model): + __table__ = "courses" + + title: str + description: str | None + price: int + + category = BelongsTo("Category") + lessons = HasMany("Lesson") + students = BelongsToMany( + "User", + local_foreign_key="course_id", + other_foreign_key="user_id", + table="course_user", + with_timestamps=True, + with_fields=["progress", "completed_at"] + ) + reviews = MorphMany("Review", "reviewable_type", "reviewable_id") diff --git a/example/database-app/app/models/lesson.py b/example/database-app/app/models/lesson.py new file mode 100644 index 00000000..c5540bf2 --- /dev/null +++ b/example/database-app/app/models/lesson.py @@ -0,0 +1,17 @@ +from typing import TYPE_CHECKING + +from fastapi_startkit.masoniteorm.models import Model +from fastapi_startkit.masoniteorm.relationships import BelongsTo, MorphMany + +if TYPE_CHECKING: + from app.models.course import Course + from app.models.review import Review + + +class Lesson(Model): + __table__ = "lessons" + + title: str + + course = BelongsTo("Course") + reviews = MorphMany("Review", "reviewable_type", "reviewable_id") diff --git a/example/database-app/app/models/media.py b/example/database-app/app/models/media.py deleted file mode 100644 index 3f30effe..00000000 --- a/example/database-app/app/models/media.py +++ /dev/null @@ -1,17 +0,0 @@ -from typing import TYPE_CHECKING - -from fastapi_startkit.masoniteorm.models import Model -from fastapi_startkit.masoniteorm.relationships import BelongsTo - -if TYPE_CHECKING: - from app.models.post import Post - - -class Media(Model): - __table__ = "media" - - id: int - post_id: int - url: str - - post: "Post" = BelongsTo("Post") diff --git a/example/database-app/app/models/post.py b/example/database-app/app/models/post.py deleted file mode 100644 index dba6972a..00000000 --- a/example/database-app/app/models/post.py +++ /dev/null @@ -1,22 +0,0 @@ -from typing import TYPE_CHECKING - -from fastapi_startkit.masoniteorm.models import Model -from fastapi_startkit.masoniteorm.relationships import BelongsTo, HasMany, BelongsToMany - -if TYPE_CHECKING: - from app.models.user import User - from app.models.tag import Tag - from app.models.media import Media - - -class Post(Model): - __table__ = "posts" - - id: int - user_id: int - title: str - content: str - - author: "User" = BelongsTo('User', local_key='user_id', foreign_key="id") - media: list["Media"] = HasMany("Media") - tags: list["Tag"] = BelongsToMany("Tag", "post_id", "tag_id", table="post_tag") diff --git a/example/database-app/app/models/post_tag.py b/example/database-app/app/models/post_tag.py deleted file mode 100644 index 55929f7b..00000000 --- a/example/database-app/app/models/post_tag.py +++ /dev/null @@ -1,9 +0,0 @@ -from fastapi_startkit.masoniteorm.models import Model - -class PostTag(Model): - __table__ = "post_tag" - __timestamps__ = False - - id: int - post_id: int - tag_id: int diff --git a/example/database-app/app/models/profile.py b/example/database-app/app/models/profile.py new file mode 100644 index 00000000..5a6088fe --- /dev/null +++ b/example/database-app/app/models/profile.py @@ -0,0 +1,25 @@ +from typing import TYPE_CHECKING + +from fastapi_startkit.masoniteorm.models import Model +from fastapi_startkit.masoniteorm.relationships import BelongsTo + +if TYPE_CHECKING: + from app.models.user import User + + +class Profile(Model): + __table__ = "profiles" + + bio: str | None + website: str | None + avatar_url: str | None + country: str | None + phone_number: str | None + headline: str | None + description: str | None + video_url: str | None + hourly_rate: int | None + languages_spoken: dict | list | None + subjects: dict | list | None + + user = BelongsTo("User") diff --git a/example/database-app/app/models/review.py b/example/database-app/app/models/review.py new file mode 100644 index 00000000..f36f2738 --- /dev/null +++ b/example/database-app/app/models/review.py @@ -0,0 +1,12 @@ +from typing import TYPE_CHECKING + +from fastapi_startkit.masoniteorm.models import Model +from fastapi_startkit.masoniteorm.relationships import MorphTo + +class Review(Model): + __table__ = "reviews" + + reviewable_type: str + content: str + + reviewable = MorphTo("Review", morph_key="reviewable_type", morph_id="reviewable_id") diff --git a/example/database-app/app/models/tag.py b/example/database-app/app/models/tag.py deleted file mode 100644 index d0cb7efb..00000000 --- a/example/database-app/app/models/tag.py +++ /dev/null @@ -1,14 +0,0 @@ -from typing import TYPE_CHECKING -from fastapi_startkit.masoniteorm.models import Model -from fastapi_startkit.masoniteorm.relationships import BelongsToMany - -if TYPE_CHECKING: - from app.models.post import Post - -class Tag(Model): - __table__ = "tags" - - id: int - name: str - - posts: list["Post"] = BelongsToMany("Post") diff --git a/example/database-app/app/models/user.py b/example/database-app/app/models/user.py index 0496ff62..81107cee 100644 --- a/example/database-app/app/models/user.py +++ b/example/database-app/app/models/user.py @@ -1,17 +1,26 @@ from typing import TYPE_CHECKING from fastapi_startkit.masoniteorm.models import Model -from fastapi_startkit.masoniteorm.relationships import HasMany +from fastapi_startkit.masoniteorm.relationships import HasMany, HasOne, BelongsToMany if TYPE_CHECKING: - from app.models.post import Post + from app.models.profile import Profile + from app.models.course import Course class User(Model): __table__ = "users" - id: int name: str email: str + role: str - posts: list["Post"] = HasMany("Post") + profile = HasOne("Profile") + courses = BelongsToMany( + "Course", + local_foreign_key="user_id", + other_foreign_key="course_id", + table="course_user", + with_timestamps=True, + with_fields=["progress", "completed_at"] + ) diff --git a/example/database-app/app/students/controllers/auth_controller.py b/example/database-app/app/students/controllers/auth_controller.py new file mode 100644 index 00000000..ef1e5984 --- /dev/null +++ b/example/database-app/app/students/controllers/auth_controller.py @@ -0,0 +1,31 @@ +import hashlib + +from fastapi import HTTPException + +from app.http.schemas.auth import StudentRegistrationRequest +from app.models import User, Profile + + +async def register(request: StudentRegistrationRequest): + existing_user = await User.where("email", request.email).first() + if existing_user: + raise HTTPException(status_code=400, detail="Email already registered") + + password = hashlib.md5(request.password.encode()).hexdigest() + user = User( + name=request.name, + email=request.email, + password=password, + role="student", + ) + await user.save() + + profile = Profile() + profile.user_id = user.id + await profile.save() + + return {"message": "Student registered successfully", "user_id": user.id} + + +def login(): + pass diff --git a/example/database-app/app/students/requests/auth.py b/example/database-app/app/students/requests/auth.py new file mode 100644 index 00000000..a3cba9f2 --- /dev/null +++ b/example/database-app/app/students/requests/auth.py @@ -0,0 +1,7 @@ +from pydantic import BaseModel, Field, EmailStr + + +class StudentRegistrationRequest(BaseModel): + name: str = Field(..., min_length=2, max_length=255) + email: EmailStr + password: str = Field(..., min_length=8) diff --git a/example/database-app/artisan b/example/database-app/artisan index 95887b0b..c4fda0c9 100755 --- a/example/database-app/artisan +++ b/example/database-app/artisan @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import sys +print("Artisan starting...") from bootstrap.application import app if __name__ == "__main__": diff --git a/example/database-app/bootstrap/application.py b/example/database-app/bootstrap/application.py index 85f1b24c..4ca635a7 100644 --- a/example/database-app/bootstrap/application.py +++ b/example/database-app/bootstrap/application.py @@ -6,15 +6,23 @@ from providers.fastapi_provider import FastAPIServiceProvider from config.app import AppConfig + +print("Loading Application class...") from fastapi_startkit.application import Application from fastapi_startkit.exceptions import ExceptionHandler from fastapi_startkit.logging.providers import LogProvider from fastapi_startkit.masoniteorm.providers import DatabaseProvider +class _FallbackHandler: + async def render(self, request, exc): + from fastapi.responses import JSONResponse + return JSONResponse(status_code=500, content={"detail": "Internal Server Error"}) + + class AppExceptionHandler(ExceptionHandler): def register(self): - pass + self.register_handler(Exception, _FallbackHandler()) app: Application[AppConfig] = Application( @@ -27,4 +35,4 @@ def register(self): FastAPIServiceProvider, ], exception_handler=AppExceptionHandler, -) +) \ No newline at end of file diff --git a/example/database-app/config/database.py b/example/database-app/config/database.py index a84762f4..a9a1998a 100644 --- a/example/database-app/config/database.py +++ b/example/database-app/config/database.py @@ -1,31 +1,21 @@ from dataclasses import field +from typing import Dict, Any +from fastapi_startkit.environment import env +from fastapi_startkit.masoniteorm import MySQLConfig, SQLiteConfig from pydantic.dataclasses import dataclass -from fastapi_startkit.environment.environment import env - - -@dataclass -class DatabaseConnection: - driver: str | None = None - host: str | None = None - database: str | None = None - username: str | None = None - password: str | None = None - port: int | None = None - prefix: str | None = None - options: dict = field(default_factory=dict) @dataclass class DatabaseConfig: - default: str = "postgres" + default: str = field(default_factory=lambda: env("DB_CONNECTION", "mysql")) - connections: dict[str, DatabaseConnection] = field(default_factory=lambda: { - "sqlite": DatabaseConnection( + connections: dict[str, Dict[str, Any]] = field(default_factory=lambda: { + "sqlite": SQLiteConfig( driver="sqlite", database=env("DB_DATABASE", "database.sqlite"), ), - "mysql": DatabaseConnection( + "mysql": MySQLConfig( driver="mysql", host=env("DB_HOST", "127.0.0.1"), database=env("DB_DATABASE", "laravel"), @@ -37,5 +27,3 @@ class DatabaseConfig: } ), }) - - migration_path: str =field(default_factory=lambda: env("MIGRATION_PATH", "database/migrations")) diff --git a/example/database-app/config/logging.py b/example/database-app/config/logging.py index a393dfe5..cf2fe44a 100644 --- a/example/database-app/config/logging.py +++ b/example/database-app/config/logging.py @@ -6,7 +6,7 @@ @dataclasses.dataclass class LoggingConfig: - default: str = dataclasses.field(default_factory=lambda: env('LOG_CHANNEL', 'syslog')) + default: str = dataclasses.field(default_factory=lambda: env('LOG_CHANNEL', 'terminal')) channels: dict = dataclasses.field(default_factory=lambda: { 'stack': StackChannel( diff --git a/example/database-app/conftest.py b/example/database-app/conftest.py new file mode 100644 index 00000000..e69de29b diff --git a/example/database-app/databases/migrations/2026_04_12_000000_create_blog_tables.py b/example/database-app/databases/migrations/2026_04_12_000000_create_blog_tables.py deleted file mode 100644 index 80698b8e..00000000 --- a/example/database-app/databases/migrations/2026_04_12_000000_create_blog_tables.py +++ /dev/null @@ -1,55 +0,0 @@ -from fastapi_startkit.masoniteorm.migrations import Migration - -class CreateBlogTables(Migration): - async def up(self): - """ - Run the migrations. - """ - # Users Table - async with await self.schema.create("users") as table: - table.increments("id") - table.string("name") - table.string("email").unique() - table.string("password") - table.timestamps() - - # Posts Table - async with await self.schema.create("posts") as table: - table.increments("id") - table.integer("user_id").unsigned() - table.foreign("user_id").references("id").on("users") - table.string("title") - table.text("content") - table.timestamps() - - # Tags Table - async with await self.schema.create("tags") as table: - table.increments("id") - table.string("name").unique() - table.timestamps() - - # Post-Tag Pivot Table - async with await self.schema.create("post_tag") as table: - table.increments("id") - table.integer("post_id").unsigned() - table.foreign("post_id").references("id").on("posts").on_delete("cascade") - table.integer("tag_id").unsigned() - table.foreign("tag_id").references("id").on("tags").on_delete("cascade") - - # Media Table - async with await self.schema.create("media") as table: - table.increments("id") - table.integer("post_id").unsigned() - table.foreign("post_id").references("id").on("posts").on_delete("cascade") - table.string("url") - table.timestamps() - - async def down(self): - """ - Revert the migrations. - """ - await self.schema.drop("media") - await self.schema.drop("post_tag") - await self.schema.drop("tags") - await self.schema.drop("posts") - await self.schema.drop("users") diff --git a/example/database-app/databases/migrations/2026_04_26_110113_create_users.py b/example/database-app/databases/migrations/2026_04_26_110113_create_users.py new file mode 100644 index 00000000..82cb2f23 --- /dev/null +++ b/example/database-app/databases/migrations/2026_04_26_110113_create_users.py @@ -0,0 +1,24 @@ +"""Create-users Migration.""" + +from fastapi_startkit.masoniteorm.migrations import Migration + + +class CreateUsers(Migration): + async def up(self): + """ + Run the migrations. + """ + async with await self.schema.create("users") as table: + table.increments("id") + table.string("email").unique() + table.string("password") + table.string("name") + table.string("role", length=50).default("student") + + table.timestamps() + + def down(self): + """ + Revert the migrations. + """ + self.schema.drop("create_users") diff --git a/example/database-app/databases/migrations/2026_04_27_145006_create_categories_table.py b/example/database-app/databases/migrations/2026_04_27_145006_create_categories_table.py new file mode 100644 index 00000000..3385379e --- /dev/null +++ b/example/database-app/databases/migrations/2026_04_27_145006_create_categories_table.py @@ -0,0 +1,20 @@ +"""CreateCategoriesTable Migration.""" + +from fastapi_startkit.masoniteorm.migrations import Migration + + +class CreateCategoriesTable(Migration): + async def up(self): + """ + Run the migrations. + """ + async with await self.schema.create("categories") as table: + table.increments("id") + table.string("name") + table.timestamps() + + async def down(self): + """ + Revert the migrations. + """ + await self.schema.drop("categories") diff --git a/example/database-app/databases/migrations/2026_04_27_145006_create_profiles_table.py b/example/database-app/databases/migrations/2026_04_27_145006_create_profiles_table.py new file mode 100644 index 00000000..2b7e9f9e --- /dev/null +++ b/example/database-app/databases/migrations/2026_04_27_145006_create_profiles_table.py @@ -0,0 +1,31 @@ +"""CreateProfilesTable Migration.""" + +from fastapi_startkit.masoniteorm.migrations import Migration + + +class CreateProfilesTable(Migration): + async def up(self): + """ + Run the migrations. + """ + async with await self.schema.create("profiles") as table: + table.increments("id") + table.integer("user_id").unsigned() + table.foreign("user_id").references("id").on("users").on_delete("cascade") + table.text("bio").nullable() + table.string("country", length=100).nullable() + table.string("phone_number", length=50).nullable() + table.string("headline", length=255).nullable() + table.text("description").nullable() + table.string("video_url").nullable() + table.integer("hourly_rate").nullable() + table.json("languages_spoken").nullable() + table.json("subjects").nullable() + + table.timestamps() + + async def down(self): + """ + Revert the migrations. + """ + await self.schema.drop("profiles") diff --git a/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py b/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py new file mode 100644 index 00000000..15c0b735 --- /dev/null +++ b/example/database-app/databases/migrations/2026_04_27_145007_create_courses_table.py @@ -0,0 +1,24 @@ +"""CreateCoursesTable Migration.""" + +from fastapi_startkit.masoniteorm.migrations import Migration + + +class CreateCoursesTable(Migration): + async def up(self): + """ + Run the migrations. + """ + async with await self.schema.create("courses") as table: + table.increments("id") + table.string("title") + table.integer("instructor_id").unsigned() + table.foreign("instructor_id").references("id").on("users").on_delete("cascade") + table.integer("category_id").unsigned().nullable() + table.foreign("category_id").references("id").on("categories").on_delete("set null") + table.timestamps() + + async def down(self): + """ + Revert the migrations. + """ + await self.schema.drop("courses") diff --git a/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py b/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py new file mode 100644 index 00000000..c1871638 --- /dev/null +++ b/example/database-app/databases/migrations/2026_04_27_145008_create_course_user_table.py @@ -0,0 +1,25 @@ +"""CreateCourseUserTable Migration.""" + +from fastapi_startkit.masoniteorm.migrations import Migration + + +class CreateCourseUserTable(Migration): + async def up(self): + """ + Run the migrations. + """ + async with await self.schema.create("course_user") as table: + table.increments("id") + table.integer("user_id").unsigned() + table.foreign("user_id").references("id").on("users").on_delete("cascade") + table.integer("course_id").unsigned() + table.foreign("course_id").references("id").on("courses").on_delete("cascade") + table.integer("progress").default(0) + table.timestamp("completed_at").nullable() + table.timestamps() + + async def down(self): + """ + Revert the migrations. + """ + await self.schema.drop("course_user") diff --git a/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py b/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py new file mode 100644 index 00000000..7d9ebfce --- /dev/null +++ b/example/database-app/databases/migrations/2026_04_27_145008_create_lessons_table.py @@ -0,0 +1,22 @@ +"""CreateLessonsTable Migration.""" + +from fastapi_startkit.masoniteorm.migrations import Migration + + +class CreateLessonsTable(Migration): + async def up(self): + """ + Run the migrations. + """ + async with await self.schema.create("lessons") as table: + table.increments("id") + table.integer("course_id").unsigned() + table.foreign("course_id").references("id").on("courses").on_delete("cascade") + table.string("title") + table.timestamps() + + async def down(self): + """ + Revert the migrations. + """ + await self.schema.drop("lessons") diff --git a/example/database-app/databases/migrations/2026_04_27_145009_create_reviews_table.py b/example/database-app/databases/migrations/2026_04_27_145009_create_reviews_table.py new file mode 100644 index 00000000..d6837241 --- /dev/null +++ b/example/database-app/databases/migrations/2026_04_27_145009_create_reviews_table.py @@ -0,0 +1,22 @@ +"""CreateReviewsTable Migration.""" + +from fastapi_startkit.masoniteorm.migrations import Migration + + +class CreateReviewsTable(Migration): + async def up(self): + """ + Run the migrations. + """ + async with await self.schema.create("reviews") as table: + table.increments("id") + table.integer("reviewable_id").unsigned() + table.string("reviewable_type") + table.text("content") + table.timestamps() + + async def down(self): + """ + Revert the migrations. + """ + await self.schema.drop("reviews") diff --git a/example/database-app/databases/seeds/category_seeder.py b/example/database-app/databases/seeds/category_seeder.py new file mode 100644 index 00000000..475f58aa --- /dev/null +++ b/example/database-app/databases/seeds/category_seeder.py @@ -0,0 +1,15 @@ +from fastapi_startkit.masoniteorm.seeds import Seeder +from app.models.category import Category + + +class CategorySeeder(Seeder): + async def run(self): + categories = [ + "Programming", + "Web Development", + "Data Science", + "Design", + "Business", + ] + for name in categories: + await Category.first_or_create({"name": name}) \ No newline at end of file diff --git a/example/database-app/databases/seeds/course_seeder.py b/example/database-app/databases/seeds/course_seeder.py new file mode 100644 index 00000000..440c5cd5 --- /dev/null +++ b/example/database-app/databases/seeds/course_seeder.py @@ -0,0 +1,59 @@ +from fastapi_startkit.masoniteorm.seeds import Seeder +from app.models.category import Category +from app.models.course import Course +from app.models.user import User +from app.models.lesson import Lesson + + +class CourseSeeder(Seeder): + async def run(self): + instructor = await User.where("email", "instructor@example.com").first() + programming = await Category.where("name", "Programming").first() + web_dev = await Category.where("name", "Web Development").first() + data_science = await Category.where("name", "Data Science").first() + + courses = [ + { + "title": "Python for Beginners", + "instructor_id": instructor.id, + "category_id": programming.id, + }, + { + "title": "FastAPI in Practice", + "instructor_id": instructor.id, + "category_id": web_dev.id, + }, + { + "title": "Intro to Data Science", + "instructor_id": instructor.id, + "category_id": data_science.id, + }, + ] + + lesson_map = { + "Python for Beginners": [ + "Variables and Types", + "Control Flow", + "Functions", + "Classes and OOP", + ], + "FastAPI in Practice": [ + "Project Setup", + "Routing and Endpoints", + "Request Validation", + "Database Integration", + ], + "Intro to Data Science": [ + "NumPy Basics", + "Pandas DataFrames", + "Data Visualisation", + "Building a Model", + ], + } + + for data in courses: + course, _ = await Course.first_or_create({"title": data["title"]}, data) + for title in lesson_map[data["title"]]: + await Lesson.first_or_create( + {"title": title, "course_id": course.id} + ) \ No newline at end of file diff --git a/example/database-app/databases/seeds/database_seeder.py b/example/database-app/databases/seeds/database_seeder.py index 7e7ef4c9..15e4c682 100644 --- a/example/database-app/databases/seeds/database_seeder.py +++ b/example/database-app/databases/seeds/database_seeder.py @@ -1,8 +1,13 @@ from fastapi_startkit.masoniteorm.seeds import Seeder +from .category_seeder import CategorySeeder from .user_seeder import UserSeeder -from .post_seeder import PostSeeder +from .course_seeder import CourseSeeder +from .review_seeder import ReviewSeeder + class DatabaseSeeder(Seeder): async def run(self): + await self.call(CategorySeeder) await self.call(UserSeeder) - await self.call(PostSeeder) + await self.call(CourseSeeder) + await self.call(ReviewSeeder) \ No newline at end of file diff --git a/example/database-app/databases/seeds/review_seeder.py b/example/database-app/databases/seeds/review_seeder.py new file mode 100644 index 00000000..29dd3f22 --- /dev/null +++ b/example/database-app/databases/seeds/review_seeder.py @@ -0,0 +1,30 @@ +from fastapi_startkit.masoniteorm.seeds import Seeder +from app.models.course import Course +from app.models.review import Review + + +class ReviewSeeder(Seeder): + async def run(self): + courses = await Course.all() + + reviews_by_title = { + "Python for Beginners": [ + "Great intro course, very clear explanations.", + "Loved the pace — perfect for someone new to Python.", + ], + "FastAPI in Practice": [ + "Hands-on and practical. Exactly what I needed.", + "Covered everything from routing to database — highly recommended.", + ], + "Intro to Data Science": [ + "Solid foundation. The Pandas section was especially useful.", + "Good course overall, could use more real-world examples.", + ], + } + + for course in courses: + contents = reviews_by_title.get(course.title, []) + for content in contents: + await Review.first_or_create( + {"reviewable_type": "courses", "reviewable_id": course.id, "content": content} + ) \ No newline at end of file diff --git a/example/database-app/databases/seeds/user_seeder.py b/example/database-app/databases/seeds/user_seeder.py index d0b51845..1cb64209 100644 --- a/example/database-app/databases/seeds/user_seeder.py +++ b/example/database-app/databases/seeds/user_seeder.py @@ -1,13 +1,50 @@ from fastapi_startkit.masoniteorm.seeds import Seeder from app.models.user import User +from app.models.profile import Profile + class UserSeeder(Seeder): async def run(self): - await User.first_or_create( - {"email": "admin@example.com"}, - {"name": "Admin User", "password": "secret"} - ) - await User.first_or_create( - {"email": "john@example.com"}, - {"name": "John Doe", "password": "secret"} - ) + users = [ + { + "email": "instructor@example.com", + "name": "Jane Smith", + "password": "secret", + "role": "instructor", + "profile": { + "bio": "Senior software engineer and educator.", + "headline": "Python & FastAPI Instructor", + "country": "US", + "hourly_rate": 80, + "languages_spoken": ["English"], + "subjects": ["Python", "FastAPI", "Data Science"], + }, + }, + { + "email": "john@example.com", + "name": "John Doe", + "password": "secret", + "role": "student", + "profile": { + "bio": "Aspiring developer learning Python.", + "country": "UK", + "languages_spoken": ["English"], + }, + }, + { + "email": "alice@example.com", + "name": "Alice Johnson", + "password": "secret", + "role": "student", + "profile": { + "bio": "Data enthusiast transitioning into tech.", + "country": "CA", + "languages_spoken": ["English", "French"], + }, + }, + ] + + for data in users: + profile_data = data.pop("profile") + user, _ = await User.first_or_create({"email": data["email"]}, data) + await Profile.first_or_create({"user_id": user.id}, {"user_id": user.id, **profile_data}) \ No newline at end of file diff --git a/example/database-app/docker-compose.yml b/example/database-app/docker-compose.yml new file mode 100644 index 00000000..c98619db --- /dev/null +++ b/example/database-app/docker-compose.yml @@ -0,0 +1,16 @@ +services: + mysql: + image: mysql:8.0 + environment: + MYSQL_ROOT_PASSWORD: rootsecret + MYSQL_DATABASE: database_app + MYSQL_USER: app + MYSQL_PASSWORD: secret + ports: + - "3306:3306" + volumes: + - mysql_data:/var/lib/mysql + - ./docker/mysql/init.sql:/docker-entrypoint-initdb.d/init.sql + +volumes: + mysql_data: diff --git a/example/database-app/docker/mysql/init.sql b/example/database-app/docker/mysql/init.sql new file mode 100644 index 00000000..330f7f29 --- /dev/null +++ b/example/database-app/docker/mysql/init.sql @@ -0,0 +1,17 @@ +-- Create the application database +CREATE DATABASE IF NOT EXISTS `database_app` + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +-- Create the application user and grant privileges +CREATE USER IF NOT EXISTS 'app'@'%' IDENTIFIED BY 'secret'; +GRANT ALL PRIVILEGES ON `database_app`.* TO 'app'@'%'; + +-- Create a separate test database +CREATE DATABASE IF NOT EXISTS `database_app_test` + CHARACTER SET utf8mb4 + COLLATE utf8mb4_unicode_ci; + +GRANT ALL PRIVILEGES ON `database_app_test`.* TO 'app'@'%'; + +FLUSH PRIVILEGES; \ No newline at end of file diff --git a/example/database-app/providers/fastapi_provider.py b/example/database-app/providers/fastapi_provider.py index fa7ad1ab..7d0b02f2 100644 --- a/example/database-app/providers/fastapi_provider.py +++ b/example/database-app/providers/fastapi_provider.py @@ -1,9 +1,12 @@ -from fastapi_startkit.fastapi.providers import FastAPIProvider -from routes.api import public +from fastapi_startkit.fastapi import FastAPIProvider class FastAPIServiceProvider(FastAPIProvider): def boot(self) -> None: super().boot() # Register routers + from routes.student import router as student + from routes.api import public + + self.app.include_router(student) self.app.include_router(public) diff --git a/example/database-app/pyproject.toml b/example/database-app/pyproject.toml index a4ad562c..9b394f4d 100644 --- a/example/database-app/pyproject.toml +++ b/example/database-app/pyproject.toml @@ -5,7 +5,9 @@ description = "FastAPI application example using fastapi-startkit with PostgreSQ readme = "README.md" requires-python = ">=3.12" dependencies = [ - "fastapi-startkit[postgres,fastapi]", + "aiomysql>=0.3.2", + "cryptography>=47.0.0", + "fastapi-startkit[database,fastapi,postgres]", ] [tool.uv.sources] @@ -14,4 +16,7 @@ fastapi-startkit = { path = "../../fastapi_startkit", editable = true } [dependency-groups] dev = [ "dumpdie>=1.5.0", + "httpx>=0.28.1", + "pytest>=9.0.3", + "pytest-asyncio>=1.3.0", ] diff --git a/example/database-app/pytest.ini b/example/database-app/pytest.ini new file mode 100644 index 00000000..2fc33af0 --- /dev/null +++ b/example/database-app/pytest.ini @@ -0,0 +1,5 @@ +[pytest] +asyncio_mode = auto +asyncio_default_fixture_loop_scope = function +asyncio_default_test_loop_scope = function +pythonpath = . diff --git a/example/database-app/readme.md b/example/database-app/readme.md new file mode 100644 index 00000000..35d0191c --- /dev/null +++ b/example/database-app/readme.md @@ -0,0 +1,192 @@ +Learning Platform System (Laravel Eloquent Relationships) + +This document describes a sample system designed to utilize all major Laravel Eloquent relationship types in a practical and scalable way. + +🧩 Overview + +A simplified learning platform where: + +Users can be students or instructors +Instructors create courses +Courses contain lessons +Students enroll in courses +Users can leave reviews on courses or lessons +🏗️ Entities +User +Profile +Course +Lesson +Enrollment (pivot) +Review +Category +🔗 Relationships +1. User +class User extends Model +{ + // One-to-One + public function profile() + { + return $this->hasOne(Profile::class); + } + + // One-to-Many (Instructor → Courses) + public function courses() + { + return $this->hasMany(Course::class, 'instructor_id'); + } + + // Many-to-Many (Student → Enrollments) + public function enrolledCourses() + { + return $this->belongsToMany(Course::class) + ->withPivot(['progress', 'completed_at']) + ->withTimestamps(); + } + + // Has Many Through (Instructor → Lessons) + public function lessons() + { + return $this->hasManyThrough( + Lesson::class, + Course::class, + 'instructor_id', + 'course_id', + 'id', + 'id' + ); + } +} +2. Profile +class Profile extends Model +{ + public function user() + { + return $this->belongsTo(User::class); + } +} +3. Course +class Course extends Model +{ + // Belongs to Instructor + public function instructor() + { + return $this->belongsTo(User::class, 'instructor_id'); + } + + // One-to-Many + public function lessons() + { + return $this->hasMany(Lesson::class); + } + + // Many-to-Many + public function students() + { + return $this->belongsToMany(User::class) + ->withPivot(['progress', 'completed_at']) + ->withTimestamps(); + } + + // Polymorphic + public function reviews() + { + return $this->morphMany(Review::class, 'reviewable'); + } + + // Category relation + public function category() + { + return $this->belongsTo(Category::class); + } +} +4. Lesson +class Lesson extends Model +{ + public function course() + { + return $this->belongsTo(Course::class); + } + + public function reviews() + { + return $this->morphMany(Review::class, 'reviewable'); + } +} +5. Enrollment (Pivot Table: course_user) +// No dedicated model required unless needed +// Table: course_user + +Schema::create('course_user', function (Blueprint $table) { + $table->id(); + $table->foreignId('user_id'); + $table->foreignId('course_id'); + $table->integer('progress')->default(0); + $table->timestamp('completed_at')->nullable(); + $table->timestamps(); +}); +6. Review (Polymorphic) +class Review extends Model +{ + public function reviewable() + { + return $this->morphTo(); + } +} +7. Category +class Category extends Model +{ + public function courses() + { + return $this->hasMany(Course::class); + } +} +🗄️ Database Structure +users +id +name +email +profiles +id +user_id +bio +courses +id +title +instructor_id +category_id +lessons +id +course_id +title +course_user (pivot) +user_id +course_id +progress +completed_at +reviews +id +reviewable_id +reviewable_type +content +categories +id +name +🧠 Relationship Summary +Type Example +hasOne User → Profile +belongsTo Course → User (Instructor) +hasMany Course → Lessons +belongsToMany User ↔ Courses (Enrollment) +hasManyThrough User → Lessons via Courses +morphMany Course/Lesson → Reviews +morphTo Review → Course/Lesson +🚀 Notes +Use with() for eager loading to avoid N+1 queries +Use pivot data for tracking progress and completion +Add indexes on foreign keys for performance +Consider policies for authorization (Instructor vs Student) +💡 Possible Extensions +Quizzes (hasMany from Lesson) +Certificates (hasOne from Enrollment) +Tags (belongsToMany) +Payments (Stripe integration) diff --git a/example/database-app/routes/api.py b/example/database-app/routes/api.py index 41eba306..8861e7f6 100644 --- a/example/database-app/routes/api.py +++ b/example/database-app/routes/api.py @@ -1,31 +1,9 @@ -from fastapi import APIRouter -from starlette.responses import JSONResponse +from fastapi_startkit.fastapi import Router -from app.models import Post -from app.models.user import User -from app.models.post import Post -public = APIRouter() +from app.students.controllers import auth_controller as student_auth +from app.http.controllers.auth_controller import AuthController -@public.get("/") -async def index(): - return {"message": "Welcome to the Database App Example!"} +public = Router() -@public.get("/users") -async def get_users(): - users = await User.first() - return JSONResponse({ - "id": users.id, - "name": users.name, - "email": users.email, - "created_at": users.created_at.diff_for_humans() - }) - -@public.get("/posts") -async def get_posts(): - # Example of fetching posts with relationships - posts = await Post.with_("author", "tags").get() - return JSONResponse([{ - 'id': post.id, - 'author': post.author.name, - 'tags': [tag.name for tag in post.tags] - } for post in posts]) +public.post("/register/student", student_auth.register) +public.post("/register/teacher", AuthController.register_teacher) \ No newline at end of file diff --git a/example/database-app/routes/student.py b/example/database-app/routes/student.py new file mode 100644 index 00000000..4280bccf --- /dev/null +++ b/example/database-app/routes/student.py @@ -0,0 +1,8 @@ +from fastapi_startkit.fastapi import Router + +from app.students.controllers import auth_controller + +router = Router() + +router.post("/students/register", auth_controller.register) +router.get("/students/login", auth_controller.login) diff --git a/example/database-app/tests/features/students/test_register.py b/example/database-app/tests/features/students/test_register.py new file mode 100644 index 00000000..93c810be --- /dev/null +++ b/example/database-app/tests/features/students/test_register.py @@ -0,0 +1,62 @@ +from app.models.user import User +from tests.test_case import TestCase, RefreshDatabase + + +class TestRegister(TestCase, RefreshDatabase): + async def test_user_can_register(self): + response = await self.post("/students/register", json={ + "name": "John Doe", + "email": "john@example.com", + "password": "password123", + }) + + assert response.status_code == 200 + assert response.json()["message"] == "Student registered successfully" + assert "user_id" in response.json() + + user = await User.where("email", "john@example.com").first() + assert user is not None + assert user.name == "John Doe" + assert user.role == "student" + + async def test_user_cannot_register_with_invalid_data(self): + # missing required fields + response = await self.post("/students/register", json={}) + assert response.status_code == 422 + + # password to shorts + response = await self.post("/students/register", json={ + "name": "John Doe", + "email": "john@example.com", + "password": "short", + }) + assert response.status_code == 422 + + # invalid email + response = await self.post("/students/register", json={ + "name": "John Doe", + "email": "not-an-email", + "password": "password123", + }) + assert response.status_code == 422 + + # name too short + response = await self.post("/students/register", json={ + "name": "J", + "email": "john@example.com", + "password": "password123", + }) + assert response.status_code == 422 + + async def test_user_cannot_register_with_duplicate_email(self): + payload = { + "name": "Jane Doe", + "email": "jane@example.com", + "password": "password123", + } + + await self.post("/students/register", json=payload) + + response = await self.post("/students/register", json=payload) + assert response.status_code == 400 + assert response.json()["detail"] == "Email already registered" diff --git a/example/database-app/tests/test_case.py b/example/database-app/tests/test_case.py new file mode 100644 index 00000000..0ef8601a --- /dev/null +++ b/example/database-app/tests/test_case.py @@ -0,0 +1,74 @@ +import gc +import pytest +from httpx import AsyncClient, ASGITransport +from bootstrap.application import app + +class TestCase: + @pytest.fixture(autouse=True) + async def setup_client(self): + async with AsyncClient( + transport=ASGITransport(app=app.fastapi), base_url="http://test" + ) as client: + self.client = client + yield + + async def get(self, url, **kwargs): + return await self.client.get(url, **kwargs) + + async def post(self, url, **kwargs): + return await self.client.post(url, **kwargs) + + async def put(self, url, **kwargs): + return await self.client.put(url, **kwargs) + + async def delete(self, url, **kwargs): + return await self.client.delete(url, **kwargs) + + +class RefreshDatabase: + migrated = False + + @staticmethod + async def migrate_database(): + from fastapi_startkit.masoniteorm.migrations import Migration + + if not RefreshDatabase.migrated: + migration = Migration(migration_directory="databases/migrations") + await migration.fresh(ignore_fk=True) + RefreshDatabase.migrated = True + + @pytest.fixture(autouse=True) + async def refresh_database(self): + await RefreshDatabase.migrate_database() + + from fastapi_startkit.masoniteorm.models import Model + db_connection = Model.db_manager.connection(None) + original_engine = db_connection.conn + + async with original_engine.connect() as conn: + transaction = await conn.begin() + + original_commit = conn.sync_connection.commit + conn.sync_connection.commit = lambda: None + + class PatchedEngine: + def connect(self): + return YieldConn() + + class YieldConn: + async def __aenter__(self): + return conn + + async def __aexit__(self, *args): + pass + + db_connection.conn = PatchedEngine() + + yield + + db_connection.conn = original_engine + conn.sync_connection.commit = original_commit + await transaction.rollback() + + await original_engine.dispose() + gc.collect() \ No newline at end of file diff --git a/example/database-app/todo.md b/example/database-app/todo.md new file mode 100644 index 00000000..b13e9469 --- /dev/null +++ b/example/database-app/todo.md @@ -0,0 +1,5 @@ +1. Tasks: +we need refresh database as +Runs migrations once at the beginning of the test suite +Then wraps each test in a database transaction +After each test → transaction is rolled back diff --git a/example/database-app/uv.lock b/example/database-app/uv.lock index 0fcacec3..a77bb569 100644 --- a/example/database-app/uv.lock +++ b/example/database-app/uv.lock @@ -7,6 +7,18 @@ resolution-markers = [ "python_full_version < '3.13'", ] +[[package]] +name = "aiomysql" +version = "0.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pymysql" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/e0/302aeffe8d90853556f47f3106b89c16cc2ec2a4d269bdfd82e3f4ae12cc/aiomysql-0.3.2.tar.gz", hash = "sha256:72d15ef5cfc34c03468eb41e1b90adb9fd9347b0b589114bd23ead569a02ac1a", size = 108311, upload-time = "2025-10-22T00:15:21.278Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/af/aae0153c3e28712adaf462328f6c7a3c196a1c1c27b491de4377dd3e6b52/aiomysql-0.3.2-py3-none-any.whl", hash = "sha256:c82c5ba04137d7afd5c693a258bea8ead2aad77101668044143a991e04632eb2", size = 71834, upload-time = "2025-10-22T00:15:15.905Z" }, +] + [[package]] name = "annotated-doc" version = "0.0.4" @@ -87,6 +99,63 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser", marker = "implementation_name != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" }, + { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" }, + { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" }, + { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" }, + { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" }, + { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" }, + { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" }, + { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" }, + { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" }, + { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" }, + { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" }, + { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" }, + { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" }, + { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" }, + { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" }, + { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" }, + { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" }, + { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" }, + { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" }, + { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" }, + { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" }, + { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" }, + { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" }, + { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -203,24 +272,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b0/5c/3ba7d12e7a79566f97b8f954400926d7b6eb33bcdccc1315a857f200f1f1/crashtest-0.4.1-py3-none-any.whl", hash = "sha256:8d23eac5fa660409f57472e3851dab7ac18aba459a8d19cbbba86d3d5aecd2a5", size = 7558, upload-time = "2022-11-02T21:15:12.437Z" }, ] +[[package]] +name = "cryptography" +version = "47.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/b2/7ffa7fe8207a8c42147ffe70c3e360b228160c1d85dc3faff16aaa3244c0/cryptography-47.0.0.tar.gz", hash = "sha256:9f8e55fe4e63613a5e1cc5819030f27b97742d720203a087802ce4ce9ceb52bb", size = 830863, upload-time = "2026-04-24T19:54:57.056Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/98/40dfe932134bdcae4f6ab5927c87488754bf9eb79297d7e0070b78dd58e9/cryptography-47.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:160ad728f128972d362e714054f6ba0067cab7fb350c5202a9ae8ae4ce3ef1a0", size = 7912214, upload-time = "2026-04-24T19:53:03.864Z" }, + { url = "https://files.pythonhosted.org/packages/34/c6/2733531243fba725f58611b918056b277692f1033373dcc8bd01af1c05d4/cryptography-47.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b9a8943e359b7615db1a3ba587994618e094ff3d6fa5a390c73d079ce18b3973", size = 4644617, upload-time = "2026-04-24T19:53:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/b27be1a670a9b87f855d211cf0e1174a5d721216b7616bd52d8581d912ed/cryptography-47.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f5c15764f261394b22aef6b00252f5195f46f2ca300bec57149474e2538b31f8", size = 4668186, upload-time = "2026-04-24T19:53:09.053Z" }, + { url = "https://files.pythonhosted.org/packages/81/b9/8443cfe5d17d482d348cee7048acf502bb89a51b6382f06240fd290d4ca3/cryptography-47.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:9c59ab0e0fa3a180a5a9c59f3a5abe3ef90d474bc56d7fadfbe80359491b615b", size = 4651244, upload-time = "2026-04-24T19:53:11.217Z" }, + { url = "https://files.pythonhosted.org/packages/5d/5e/13ed0cdd0eb88ba159d6dd5ebfece8cb901dbcf1ae5ac4072e28b55d3153/cryptography-47.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:34b4358b925a5ea3e14384ca781a2c0ef7ac219b57bb9eacc4457078e2b19f92", size = 5252906, upload-time = "2026-04-24T19:53:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/64/16/ed058e1df0f33d440217cd120d41d5dda9dd215a80b8187f68483185af82/cryptography-47.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0024b87d47ae2399165a6bfb20d24888881eeab83ae2566d62467c5ff0030ce7", size = 4701842, upload-time = "2026-04-24T19:53:15.618Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3d30986b30fdbd9e969abbdf8ba00ed0618615144341faeb57f395a084fe/cryptography-47.0.0-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:1e47422b5557bb82d3fff997e8d92cff4e28b9789576984f08c248d2b3535d93", size = 4289313, upload-time = "2026-04-24T19:53:17.755Z" }, + { url = "https://files.pythonhosted.org/packages/df/fd/32db38e3ad0cb331f0691cb4c7a8a6f176f679124dee746b3af6633db4d9/cryptography-47.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:6f29f36582e6151d9686235e586dd35bb67491f024767d10b842e520dc6a07ac", size = 4650964, upload-time = "2026-04-24T19:53:20.062Z" }, + { url = "https://files.pythonhosted.org/packages/86/53/5395d944dfd48cb1f67917f533c609c34347185ef15eb4308024c876f274/cryptography-47.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a9b761f012a943b7de0e828843c5688d0de94a0578d44d6c85a1bae32f87791f", size = 5207817, upload-time = "2026-04-24T19:53:22.498Z" }, + { url = "https://files.pythonhosted.org/packages/34/4f/e5711b28e1901f7d480a2b1b688b645aa4c77c73f10731ed17e7f7db3f0d/cryptography-47.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4e1de79e047e25d6e9f8cea71c86b4a53aced64134f0f003bbcbf3655fd172c8", size = 4701544, upload-time = "2026-04-24T19:53:24.356Z" }, + { url = "https://files.pythonhosted.org/packages/22/22/c8ddc25de3010fc8da447648f5a092c40e7a8fadf01dd6d255d9c0b9373d/cryptography-47.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ef6b3634087f18d2155b1e8ce264e5345a753da2c5fa9815e7d41315c90f8318", size = 4783536, upload-time = "2026-04-24T19:53:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/66/b6/d4a68f4ea999c6d89e8498579cba1c5fcba4276284de7773b17e4fa69293/cryptography-47.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:11dbb9f50a0f1bb9757b3d8c27c1101780efb8f0bdecfb12439c22a74d64c001", size = 4926106, upload-time = "2026-04-24T19:53:28.686Z" }, + { url = "https://files.pythonhosted.org/packages/54/ed/5f524db1fade9c013aa618e1c99c6ed05e8ffc9ceee6cda22fed22dda3f4/cryptography-47.0.0-cp311-abi3-win32.whl", hash = "sha256:7fda2f02c9015db3f42bb8a22324a454516ed10a8c29ca6ece6cdbb5efe2a203", size = 3258581, upload-time = "2026-04-24T19:53:31.058Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dc/1b901990b174786569029f67542b3edf72ac068b6c3c8683c17e6a2f5363/cryptography-47.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:f5c3296dab66202f1b18a91fa266be93d6aa0c2806ea3d67762c69f60adc71aa", size = 3775309, upload-time = "2026-04-24T19:53:33.054Z" }, + { url = "https://files.pythonhosted.org/packages/14/88/7aa18ad9c11bc87689affa5ce4368d884b517502d75739d475fc6f4a03c7/cryptography-47.0.0-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:be12cb6a204f77ed968bcefe68086eb061695b540a3dd05edac507a3111b25f0", size = 7904299, upload-time = "2026-04-24T19:53:35.003Z" }, + { url = "https://files.pythonhosted.org/packages/07/55/c18f75724544872f234678fdedc871391722cb34a2aee19faa9f63100bb2/cryptography-47.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2ebd84adf0728c039a3be2700289378e1c164afc6748df1a5ed456767bef9ba7", size = 4631180, upload-time = "2026-04-24T19:53:37.517Z" }, + { url = "https://files.pythonhosted.org/packages/ee/65/31a5cc0eaca99cec5bafffe155d407115d96136bb161e8b49e0ef73f09a7/cryptography-47.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f68d6fbc7fbbcfb0939fea72c3b96a9f9a6edfc0e1b1d29778a2066030418b1", size = 4653529, upload-time = "2026-04-24T19:53:39.775Z" }, + { url = "https://files.pythonhosted.org/packages/e5/bc/641c0519a495f3bfd0421b48d7cd325c4336578523ccd76ea322b6c29c7a/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:6651d32eff255423503aa276739da98c30f26c40cbeffcc6048e0d54ef704c0c", size = 4638570, upload-time = "2026-04-24T19:53:42.129Z" }, + { url = "https://files.pythonhosted.org/packages/2b/f2/300327b0a47f6dc94dd8b71b57052aefe178bb51745073d73d80604f11ab/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:3fb8fa48075fad7193f2e5496135c6a76ac4b2aa5a38433df0a539296b377829", size = 5238019, upload-time = "2026-04-24T19:53:44.577Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5a/5b5cf994391d4bf9d9c7efd4c66aabe4d95227256627f8fea6cff7dfadbd/cryptography-47.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:11438c7518132d95f354fa01a4aa2f806d172a061a7bed18cf18cbdacdb204d7", size = 4686832, upload-time = "2026-04-24T19:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/dc/2c/ae950e28fd6475c852fc21a44db3e6b5bcc1261d1e370f2b6e42fa800fef/cryptography-47.0.0-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:8c1a736bbb3288005796c3f7ccb9453360d7fed483b13b9f468aea5171432923", size = 4269301, upload-time = "2026-04-24T19:53:48.97Z" }, + { url = "https://files.pythonhosted.org/packages/67/fb/6a39782e150ffe5cc1b0018cb6ddc48bf7ca62b498d7539ffc8a758e977d/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:f1557695e5c2b86e204f6ce9470497848634100787935ab7adc5397c54abd7ab", size = 4638110, upload-time = "2026-04-24T19:53:51.011Z" }, + { url = "https://files.pythonhosted.org/packages/8e/d7/0b3c71090a76e5c203164a47688b697635ece006dcd2499ab3a4dbd3f0bd/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:f9a034b642b960767fb343766ae5ba6ad653f2e890ddd82955aef288ffea8736", size = 5194988, upload-time = "2026-04-24T19:53:52.962Z" }, + { url = "https://files.pythonhosted.org/packages/63/33/63a961498a9df51721ab578c5a2622661411fc520e00bd83b0cc64eb20c4/cryptography-47.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:b1c76fca783aa7698eb21eb14f9c4aa09452248ee54a627d125025a43f83e7a7", size = 4686563, upload-time = "2026-04-24T19:53:55.274Z" }, + { url = "https://files.pythonhosted.org/packages/b7/bf/5ee5b145248f92250de86145d1c1d6edebbd57a7fe7caa4dedb5d4cf06a1/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4f7722c97826770bab8ae92959a2e7b20a5e9e9bf4deae68fd86c3ca457bab52", size = 4770094, upload-time = "2026-04-24T19:53:57.753Z" }, + { url = "https://files.pythonhosted.org/packages/92/43/21d220b2da5d517773894dacdcdb5c682c28d3fffce65548cb06e87d5501/cryptography-47.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:09f6d7bf6724f8db8b32f11eccf23efc8e759924bc5603800335cf8859a3ddbd", size = 4913811, upload-time = "2026-04-24T19:54:00.236Z" }, + { url = "https://files.pythonhosted.org/packages/31/98/dc4ad376ac5f1a1a7d4a83f7b0c6f2bcad36b5d2d8f30aeb482d3a7d9582/cryptography-47.0.0-cp314-cp314t-win32.whl", hash = "sha256:6eebcaf0df1d21ce1f90605c9b432dd2c4f4ab665ac29a40d5e3fc68f51b5e63", size = 3237158, upload-time = "2026-04-24T19:54:02.606Z" }, + { url = "https://files.pythonhosted.org/packages/bc/da/97f62d18306b5133468bc3f8cc73a3111e8cdc8cf8d3e69474d6e5fd2d1b/cryptography-47.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:51c9313e90bd1690ec5a75ed047c27c0b8e6c570029712943d6116ef9a90620b", size = 3758706, upload-time = "2026-04-24T19:54:04.433Z" }, + { url = "https://files.pythonhosted.org/packages/e0/34/a4fae8ae7c3bc227460c9ae43f56abf1b911da0ec29e0ebac53bb0a4b6b7/cryptography-47.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:14432c8a9bcb37009784f9594a62fae211a2ae9543e96c92b2a8e4c3cd5cd0c4", size = 7904072, upload-time = "2026-04-24T19:54:06.411Z" }, + { url = "https://files.pythonhosted.org/packages/01/64/d7b1e54fdb69f22d24a64bb3e88dc718b31c7fb10ef0b9691a3cf7eeea6e/cryptography-47.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:07efe86201817e7d3c18781ca9770bc0db04e1e48c994be384e4602bc38f8f27", size = 4635767, upload-time = "2026-04-24T19:54:08.519Z" }, + { url = "https://files.pythonhosted.org/packages/8b/7b/cca826391fb2a94efdcdfe4631eb69306ee1cff0b22f664a412c90713877/cryptography-47.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b45761c6ec22b7c726d6a829558777e32d0f1c8be7c3f3480f9c912d5ee8a10", size = 4654350, upload-time = "2026-04-24T19:54:10.795Z" }, + { url = "https://files.pythonhosted.org/packages/4c/65/4b57bcc823f42a991627c51c2f68c9fd6eb1393c1756aac876cba2accae2/cryptography-47.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:edd4da498015da5b9f26d38d3bfc2e90257bfa9cbed1f6767c282a0025ae649b", size = 4643394, upload-time = "2026-04-24T19:54:13.275Z" }, + { url = "https://files.pythonhosted.org/packages/f4/c4/2c5fbeea70adbbca2bbae865e1d605d6a4a7f8dbd9d33eaf69645087f06c/cryptography-47.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:9af828c0d5a65c70ec729cd7495a4bf1a67ecb66417b8f02ff125ab8a6326a74", size = 5225777, upload-time = "2026-04-24T19:54:15.18Z" }, + { url = "https://files.pythonhosted.org/packages/7e/b8/ac57107ef32749d2b244e36069bb688792a363aaaa3acc9e3cf84c130315/cryptography-47.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:256d07c78a04d6b276f5df935a9923275f53bd1522f214447fdf365494e2d515", size = 4688771, upload-time = "2026-04-24T19:54:17.835Z" }, + { url = "https://files.pythonhosted.org/packages/56/fc/9f1de22ff8be99d991f240a46863c52d475404c408886c5a38d2b5c3bb26/cryptography-47.0.0-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:5d0e362ff51041b0c0d219cc7d6924d7b8996f57ce5712bdcef71eb3c65a59cc", size = 4270753, upload-time = "2026-04-24T19:54:19.963Z" }, + { url = "https://files.pythonhosted.org/packages/00/68/d70c852797aa68e8e48d12e5a87170c43f67bb4a59403627259dd57d15de/cryptography-47.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:1581aef4219f7ca2849d0250edaa3866212fb74bf5667284f46aa92f9e65c1ca", size = 4642911, upload-time = "2026-04-24T19:54:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/661cbee74f594c5d97ff82d34f10d5551c085ca4668645f4606ebd22bd5d/cryptography-47.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:a49a3eb5341b9503fa3000a9a0db033161db90d47285291f53c2a9d2cd1b7f76", size = 5181411, upload-time = "2026-04-24T19:54:24.376Z" }, + { url = "https://files.pythonhosted.org/packages/94/87/f2b6c374a82cf076cfa1416992ac8e8ec94d79facc37aec87c1a5cb72352/cryptography-47.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:2207a498b03275d0051589e326b79d4cf59985c99031b05bb292ac52631c37fe", size = 4688262, upload-time = "2026-04-24T19:54:26.946Z" }, + { url = "https://files.pythonhosted.org/packages/14/e2/8b7462f4acf21ec509616f0245018bb197194ab0b65c2ea21a0bdd53c0eb/cryptography-47.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:7a02675e2fabd0c0fc04c868b8781863cbf1967691543c22f5470500ff840b31", size = 4775506, upload-time = "2026-04-24T19:54:28.926Z" }, + { url = "https://files.pythonhosted.org/packages/70/75/158e494e4c08dc05e039da5bb48553826bd26c23930cf8d3cd5f21fa8921/cryptography-47.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80887c5cbd1774683cb126f0ab4184567f080071d5acf62205acb354b4b753b7", size = 4912060, upload-time = "2026-04-24T19:54:30.869Z" }, + { url = "https://files.pythonhosted.org/packages/06/bd/0a9d3edbf5eadbac926d7b9b3cd0c4be584eeeae4a003d24d9eda4affbbd/cryptography-47.0.0-cp38-abi3-win32.whl", hash = "sha256:ed67ea4e0cfb5faa5bc7ecb6e2b8838f3807a03758eec239d6c21c8769355310", size = 3248487, upload-time = "2026-04-24T19:54:33.494Z" }, + { url = "https://files.pythonhosted.org/packages/60/80/5681af756d0da3a599b7bdb586fac5a1540f1bcefd2717a20e611ddade45/cryptography-47.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:835d2d7f47cdc53b3224e90810fb1d36ca94ea29cc1801fb4c1bc43876735769", size = 3755737, upload-time = "2026-04-24T19:54:35.408Z" }, +] + [[package]] name = "database-app" version = "0.1.0" source = { virtual = "." } dependencies = [ - { name = "fastapi-startkit", extra = ["fastapi", "postgres"] }, + { name = "aiomysql" }, + { name = "cryptography" }, + { name = "fastapi-startkit", extra = ["database", "fastapi", "postgres"] }, ] [package.dev-dependencies] dev = [ { name = "dumpdie" }, + { name = "httpx" }, + { name = "pytest" }, + { name = "pytest-asyncio" }, ] [package.metadata] -requires-dist = [{ name = "fastapi-startkit", extras = ["postgres", "fastapi"], editable = "../../fastapi_startkit" }] +requires-dist = [ + { name = "aiomysql", specifier = ">=0.3.2" }, + { name = "cryptography", specifier = ">=47.0.0" }, + { name = "fastapi-startkit", extras = ["database", "fastapi", "postgres"], editable = "../../fastapi_startkit" }, +] [package.metadata.requires-dev] -dev = [{ name = "dumpdie", specifier = ">=1.5.0" }] +dev = [ + { name = "dumpdie", specifier = ">=1.5.0" }, + { name = "httpx", specifier = ">=0.28.1" }, + { name = "pytest", specifier = ">=9.0.3" }, + { name = "pytest-asyncio", specifier = ">=1.3.0" }, +] [[package]] name = "dnspython" @@ -273,6 +409,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "faker" +version = "40.15.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/13/6741787bd91c4109c7bed047d68273965cd52ce8a5f773c471b949334b6d/faker-40.15.0.tar.gz", hash = "sha256:20f3a6ec8c266b74d4c554e34118b21c3c2056c0b4a519d15c8decb3a4e6e795", size = 1967447, upload-time = "2026-04-17T20:05:27.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/a7/a600f8f30d4505e89166de51dd121bd540ab8e560e8cf0901de00a81de8c/faker-40.15.0-py3-none-any.whl", hash = "sha256:71ab3c3370da9d2205ab74ffb0fd51273063ad562b3a3bb69d0026a20923e318", size = 2004447, upload-time = "2026-04-17T20:05:25.437Z" }, +] + [[package]] name = "fastapi" version = "0.124.4" @@ -339,7 +487,7 @@ wheels = [ [[package]] name = "fastapi-startkit" -version = "0.1.3" +version = "0.13.6" source = { editable = "../../fastapi_startkit" } dependencies = [ { name = "cleo" }, @@ -347,10 +495,15 @@ dependencies = [ { name = "dotty-dict" }, { name = "inflection" }, { name = "pendulum" }, + { name = "pydantic" }, { name = "requests" }, ] [package.optional-dependencies] +database = [ + { name = "faker" }, + { name = "sqlalchemy", extra = ["asyncio"] }, +] fastapi = [ { name = "fastapi", extra = ["standard"] }, ] @@ -369,18 +522,20 @@ requires-dist = [ { name = "faker", marker = "extra == 'database'", specifier = ">=40.13.0" }, { name = "fastapi", extras = ["standard"], marker = "extra == 'fastapi'", specifier = ">=0.124.4,<0.125.0" }, { name = "inflection", specifier = ">=0.5.1" }, + { name = "jinja2", marker = "extra == 'vite'", specifier = ">=3.1" }, { name = "pendulum", specifier = ">=3.1.0,<4.0.0" }, - { name = "pydantic", marker = "extra == 'database'" }, + { name = "pydantic", specifier = ">=2.12.5" }, { name = "requests", specifier = ">=2.32.5,<3.0.0" }, { name = "sqlalchemy", extras = ["asyncio"], marker = "extra == 'database'", specifier = ">=2.0.38" }, ] -provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql"] +provides-extras = ["fastapi", "database", "sqlite", "postgres", "mysql", "vite"] [package.metadata.requires-dev] dev = [ { name = "dumpdie", specifier = ">=1.5.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, + { name = "twine", specifier = ">=6.2.0" }, ] [[package]] @@ -455,6 +610,53 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/71/4f/a5b3cfc3e1779c756c4fef790facef12d63088402a718b167b780d4f2ccc/fastar-0.10.0-cp314-cp314t-win_arm64.whl", hash = "sha256:3152a80835ef11cbfaf153037dd694e0014f25d074792b9785421679acdbe179", size = 461150, upload-time = "2026-04-08T01:02:08.47Z" }, ] +[[package]] +name = "greenlet" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3c/3f/dbf99fb14bfeb88c28f16729215478c0e265cacd6dc22270c8f31bb6892f/greenlet-3.5.0.tar.gz", hash = "sha256:d419647372241bc68e957bf38d5c1f98852155e4146bd1e4121adea81f4f01e4", size = 196995, upload-time = "2026-04-27T13:37:15.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/32/f2ce6d4cac3e55bc6173f92dbe627e782e1850f89d986c3606feb63aafa7/greenlet-3.5.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:db2910d3c809444e0a20147361f343fe2798e106af8d9d8506f5305302655a9f", size = 286228, upload-time = "2026-04-27T12:20:34.421Z" }, + { url = "https://files.pythonhosted.org/packages/b7/aa/caed9e5adf742315fc7be2a84196373aab4816e540e38ba0d76cb7584d68/greenlet-3.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3ec9ea74e7268ace7f9aab1b1a4e730193fc661b39a993cd91c606c32d4a3628", size = 601775, upload-time = "2026-04-27T12:52:41.045Z" }, + { url = "https://files.pythonhosted.org/packages/c7/af/90ae08497400a941595d12774447f752d3dfe0fbb012e35b76bc5c0ff37e/greenlet-3.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:54d243512da35485fc7a6bf3c178fdda6327a9d6506fcdd62b1abd1e41b2927b", size = 614436, upload-time = "2026-04-27T12:59:41.595Z" }, + { url = "https://files.pythonhosted.org/packages/3f/e9/4eeadf8cb3403ac274245ba75f07844abc7fa5f6787583fc9156ba741e0f/greenlet-3.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:41353ec2ecedf7aa8f682753a41919f8718031a6edac46b8d3dc7ed9e1ceb136", size = 620610, upload-time = "2026-04-27T13:02:39.194Z" }, + { url = "https://files.pythonhosted.org/packages/2b/e0/2e13df68f367e2f9960616927d60857dd7e56aaadd59a47c644216b2f920/greenlet-3.5.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d280a7f5c331622c69f97eb167f33577ff2d1df282c41cd15907fc0a3ca198c", size = 611388, upload-time = "2026-04-27T12:25:28.008Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ef/f913b3c0eb7d26d86a2401c5e1546c9d46b657efee724b06f6f4ac5d8824/greenlet-3.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:58c1c374fe2b3d852f9b6b11a7dff4c85404e51b9a596fd9e89cf904eb09866d", size = 422775, upload-time = "2026-04-27T13:05:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/82/f7/393c64055132ac0d488ef6be549253b7e6274194863967ddc0bc8f5b87b8/greenlet-3.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1eb67d5adefb5bd2e182d42678a328979a209e4e82eb93575708185d31d1f588", size = 1570768, upload-time = "2026-04-27T12:53:28.099Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4b/eaf7735253522cf56d1b74d672a58f54fc114702ceaf05def59aae72f6e1/greenlet-3.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2628d6c86f6cb0cb45e0c3c54058bbec559f57eaae699447748cb3928150577e", size = 1635983, upload-time = "2026-04-27T12:25:26.903Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fe/4fb3a0805bd5165da5ebf858da7cc01cce8061674106d2cf5bdab32cbfde/greenlet-3.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:d4d9f0624c775f2dfc56ba54d515a8c771044346852a918b405914f6b19d7fd8", size = 238840, upload-time = "2026-04-27T12:23:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/cb/baa584cb00532126ffe12d9787db0a60c5a4f55c27bfe2666df5d4c30a32/greenlet-3.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:83ed9f27f1680b50e89f40f6df348a290ea234b249a4003d366663a12eab94f2", size = 235615, upload-time = "2026-04-27T12:21:38.57Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/fc576f99037ce19c5aa16628e4c3226b6d1419f72a62c79f5f40576e6eb3/greenlet-3.5.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:5a5ed18de6a0f6cc7087f1563f6bd93fc7df1c19165ca01e9bde5a5dc281d106", size = 285066, upload-time = "2026-04-27T12:23:05.033Z" }, + { url = "https://files.pythonhosted.org/packages/4a/ba/b28ddbe6bfad6a8ac196ef0e8cff37bc65b79735995b9e410923fffeeb70/greenlet-3.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a717fbc46d8a354fa675f7c1e813485b6ba3885f9bef0cd56e5ba27d758ff5b", size = 604414, upload-time = "2026-04-27T12:52:42.358Z" }, + { url = "https://files.pythonhosted.org/packages/09/06/4b69f8f0b67603a8be2790e55107a190b376f2627fe0eaf5695d85ffb3cd/greenlet-3.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ddc090c5c1792b10246a78e8c2163ebbe04cf877f9d785c230a7b27b39ad038e", size = 617349, upload-time = "2026-04-27T12:59:43.32Z" }, + { url = "https://files.pythonhosted.org/packages/6a/15/a643b4ecd09969e30b8a150d5919960caae0abe4f5af75ab040b1ab85e78/greenlet-3.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:4964101b8585c144cbda5532b1aa644255126c08a265dae90c16e7a0e63aaa9d", size = 623234, upload-time = "2026-04-27T13:02:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/a3918541fd0ddefe024a69de6d16aa7b46d36ac19562adaa63c7fa180eff/greenlet-3.5.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2094acd54b272cb6eae8c03dd87b3fa1820a4cef18d6889c378d503500a1dc13", size = 613927, upload-time = "2026-04-27T12:25:30.28Z" }, + { url = "https://files.pythonhosted.org/packages/77/18/3b13d5ef1275b0ffaf933b05efa21408ac4ca95823c7411d79682e4fdcff/greenlet-3.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:7022615368890680e67b9965d33f5773aade330d5343bbe25560135aaa849eae", size = 425243, upload-time = "2026-04-27T13:05:15.689Z" }, + { url = "https://files.pythonhosted.org/packages/ee/e1/bd0af6213c7dd33175d8a462d4c1fe1175124ebed4855bc1475a5b5242c2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:5e05ba267789ea87b5a155cf0e810b1ab88bf18e9e8740813945ceb8ee4350ba", size = 1570893, upload-time = "2026-04-27T12:53:29.483Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2a/0789702f864f5382cb476b93d7a9c823c10472658102ccd65f415747d2e2/greenlet-3.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0ecec963079cd58cbd14723582384f11f166fd58883c15dcbfb342e0bc9b5846", size = 1636060, upload-time = "2026-04-27T12:25:28.845Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8f/22bf9df92bbff0eb07842b60f7e63bf7675a9742df628437a9f02d09137f/greenlet-3.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:728d9667d8f2f586644b748dbd9bb67e50d6a9381767d1357714ea6825bb3bf5", size = 238740, upload-time = "2026-04-27T12:24:01.341Z" }, + { url = "https://files.pythonhosted.org/packages/b6/b7/9c5c3d653bd4ff614277c049ac676422e2c557db47b4fe43e6313fc005dc/greenlet-3.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:47422135b1d308c14b2c6e758beedb1acd33bb91679f5670edf77bf46244722b", size = 235525, upload-time = "2026-04-27T12:23:12.308Z" }, + { url = "https://files.pythonhosted.org/packages/94/5e/a70f31e3e8d961c4ce589c15b28e4225d63704e431a23932a3808cbcc867/greenlet-3.5.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:f35807464c4c58c55f0d31dfa83c541a5615d825c2fe3d2b95360cf7c4e3c0a8", size = 285564, upload-time = "2026-04-27T12:23:08.555Z" }, + { url = "https://files.pythonhosted.org/packages/af/a6/046c0a28e21833e4086918218cfb3d8bed51c075a1b700f20b9d7861c0f4/greenlet-3.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:55fa7ea52771be44af0de27d8b80c02cd18c2c3cddde6c847ecebdf72418b6a1", size = 651166, upload-time = "2026-04-27T12:52:43.644Z" }, + { url = "https://files.pythonhosted.org/packages/47/f8/4af27f71c5ff32a7fbc516adb46370d9c4ae2bc7bd3dc7d066ac542b4b15/greenlet-3.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a97e4821aa710603f94de0da25f25096454d78ffdace5dc77f3a006bc01abba3", size = 663792, upload-time = "2026-04-27T12:59:44.93Z" }, + { url = "https://files.pythonhosted.org/packages/fb/89/2dadb89793c37ee8b4c237857188293e9060dc085f19845c292e00f8e091/greenlet-3.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:bf2d8a80bec89ab46221ae45c5373d5ba0bd36c19aa8508e85c6cd7e5106cd37", size = 668086, upload-time = "2026-04-27T13:02:42.314Z" }, + { url = "https://files.pythonhosted.org/packages/a3/59/1bd6d7428d6ed9106efbb8c52310c60fd04f6672490f452aeaa3829aa436/greenlet-3.5.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8f52a464e4ed91780bdfbbdd2b97197f3accaa629b98c200f4dffada759f3ae7", size = 660933, upload-time = "2026-04-27T12:25:33.276Z" }, + { url = "https://files.pythonhosted.org/packages/82/35/75722be7e26a2af4cbd2dc35b0ed382dacf9394b7e75551f76ed1abe87f2/greenlet-3.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:1bae92a1dd94c5f9d9493c3a212dd874c202442047cf96446412c862feca83a2", size = 470799, upload-time = "2026-04-27T13:05:17.094Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/b903e5a5fae1e8a28cdd32a0cfbfd560b668c25b692f67768822ddc5f40f/greenlet-3.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:762612baf1161ccb8437c0161c668a688223cba28e1bf038f4eb47b13e39ccdf", size = 1618401, upload-time = "2026-04-27T12:53:31.062Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e3/5ec408a329acb854fb607a122e1ee5fb3ff649f9a97952948a90803c0d8e/greenlet-3.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57a43c6079a89713522bc4bcb9f75070ecf5d3dbad7792bfe42239362cbf2a16", size = 1682038, upload-time = "2026-04-27T12:25:31.838Z" }, + { url = "https://files.pythonhosted.org/packages/91/20/6b165108058767ee643c55c5c4904d591a830ee2b3c7dbd359828fbc829f/greenlet-3.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:3bc59be3945ae9750b9e7d45067d01ae3fe90ea5f9ade99239dabdd6e28a5033", size = 239835, upload-time = "2026-04-27T12:24:54.136Z" }, + { url = "https://files.pythonhosted.org/packages/4e/62/1c498375cee177b55d980c1db319f26470e5309e54698c8f8fc06c0fd539/greenlet-3.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:a96fcee45e03fe30a62669fd16ab5c9d3c172660d3085605cb1e2d1280d3c988", size = 236862, upload-time = "2026-04-27T12:23:24.957Z" }, + { url = "https://files.pythonhosted.org/packages/78/a8/4522939255bb5409af4e87132f915446bf3622c2c292d14d3c38d128ae82/greenlet-3.5.0-cp314-cp314t-macosx_11_0_universal2.whl", hash = "sha256:a10a732421ab4fec934783ce3e54763470d0181db6e3468f9103a275c3ed1853", size = 293614, upload-time = "2026-04-27T12:24:12.874Z" }, + { url = "https://files.pythonhosted.org/packages/15/5e/8744c52e2c027b5a8772a01561934c8835f869733e101f62075c60430340/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fc391b1566f2907d17aaebe78f8855dc45675159a775fcf9e61f8ee0078e87f", size = 650723, upload-time = "2026-04-27T12:52:45.412Z" }, + { url = "https://files.pythonhosted.org/packages/00/ef/7b4c39c03cf46ceca512c5d3f914afd85aa30b2cc9a93015b0dd73e4be6c/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:680bd0e7ad5e8daa8a4aa89f68fd6adc834b8a8036dc256533f7e08f4a4b01f7", size = 656529, upload-time = "2026-04-27T12:59:46.295Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5c/0602239503b124b70e39355cbdb39361ecfe65b87a5f2f63752c32f5286f/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1aa4ce8debcd4ea7fb2e150f3036588c41493d1d52c43538924ae1819003f4ce", size = 657015, upload-time = "2026-04-27T13:02:43.973Z" }, + { url = "https://files.pythonhosted.org/packages/0b/b5/c7768f352f5c010f92064d0063f987e7dc0cd290a6d92a34109015ce4aa1/greenlet-3.5.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ddb36c7d6c9c0a65f18c7258634e0c416c6ab59caac8c987b96f80c2ebda0112", size = 654364, upload-time = "2026-04-27T12:25:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/38/51/8699f865f125dc952384cb432b0f7138aa4d8f2969a7d12d0df5b94d054d/greenlet-3.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:728a73687e39ae9ca34e4694cbf2f049d3fbc7174639468d0f67200a97d8f9e2", size = 488275, upload-time = "2026-04-27T13:05:18.28Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d0/079ebe12e4b1fc758857ce5be1a5e73f06870f2101e52611d1e71925ce54/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e5ddf316ced87539144621453c3aef229575825fe60c604e62bedc4003f372b2", size = 1614204, upload-time = "2026-04-27T12:53:32.618Z" }, + { url = "https://files.pythonhosted.org/packages/6d/89/6c2fb63df3596552d20e58fb4d96669243388cf680cff222758812c7bfaa/greenlet-3.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4a448128607be0de65342dc9b31be7f948ef4cc0bc8832069350abefd310a8f2", size = 1675480, upload-time = "2026-04-27T12:25:34.168Z" }, + { url = "https://files.pythonhosted.org/packages/15/32/77ee8a6c1564fc345a491a4e85b3bf360e4cf26eac98c4532d2fdb96e01f/greenlet-3.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d60097128cb0a1cab9ea541186ea13cd7b847b8449a7787c2e2350da0cb82d86", size = 245324, upload-time = "2026-04-27T12:24:40.295Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -539,6 +741,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/91/aa6bde563e0085a02a435aa99b49ef75b0a4b062635e606dab23ce18d720/inflection-0.5.1-py2.py3-none-any.whl", hash = "sha256:f38b2b640938a4f35ade69ac3d053042959b62a0f1076a5bbaa1b9526605a8a2", size = 9454, upload-time = "2020-08-22T08:16:27.816Z" }, ] +[[package]] +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -635,6 +846,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, ] +[[package]] +name = "packaging" +version = "26.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" }, +] + [[package]] name = "pendulum" version = "3.2.0" @@ -678,6 +898,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/02/fb/d65db067a67df7252f18b0cb7420dda84078b9e8bfb375215469c14a50be/pendulum-3.2.0-py3-none-any.whl", hash = "sha256:f3a9c18a89b4d9ef39c5fa6a78722aaff8d5be2597c129a3b16b9f40a561acf3", size = 114111, upload-time = "2026-01-30T11:22:22.361Z" }, ] +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "pycparser" +version = "3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" }, +] + [[package]] name = "pydantic" version = "2.12.5" @@ -778,6 +1016,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pymysql" +version = "1.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f5/ae/1fe3fcd9f959efa0ebe200b8de88b5a5ce3e767e38c7ac32fb179f16a388/pymysql-1.1.2.tar.gz", hash = "sha256:4961d3e165614ae65014e361811a724e2044ad3ea3739de9903ae7c21f539f03", size = 48258, upload-time = "2025-08-24T12:55:55.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/4c/ad33b92b9864cbde84f259d5df035a6447f91891f5be77788e2a3892bce3/pymysql-1.1.2-py3-none-any.whl", hash = "sha256:e6b1d89711dd51f8f74b1631fe08f039e7d76cf67a42a323d3178f0f25762ed9", size = 45300, upload-time = "2025-08-24T12:55:53.394Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" }, +] + +[[package]] +name = "pytest-asyncio" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytest" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/90/2c/8af215c0f776415f3590cac4f9086ccefd6fd463befeae41cd4d3f193e5a/pytest_asyncio-1.3.0.tar.gz", hash = "sha256:d7f52f36d231b80ee124cd216ffb19369aa168fc10095013c6b014a34d3ee9e5", size = 50087, upload-time = "2025-11-10T16:07:47.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/35/f8b19922b6a25bc0880171a2f1a003eaeb93657475193ab516fd87cac9da/pytest_asyncio-1.3.0-py3-none-any.whl", hash = "sha256:611e26147c7f77640e6d0a92a38ed17c3e9848063698d5c93d5aa7aa11cebff5", size = 15075, upload-time = "2025-11-10T16:07:45.537Z" }, +] + [[package]] name = "python-dateutil" version = "2.9.0.post0" @@ -1058,6 +1334,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] +[[package]] +name = "sqlalchemy" +version = "2.0.49" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "greenlet", marker = "platform_machine == 'AMD64' or platform_machine == 'WIN32' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'ppc64le' or platform_machine == 'win32' or platform_machine == 'x86_64'" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/45/461788f35e0364a8da7bda51a1fe1b09762d0c32f12f63727998d85a873b/sqlalchemy-2.0.49.tar.gz", hash = "sha256:d15950a57a210e36dd4cec1aac22787e2a4d57ba9318233e2ef8b2daf9ff2d5f", size = 9898221, upload-time = "2026-04-03T16:38:11.704Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/b3/2de412451330756aaaa72d27131db6dde23995efe62c941184e15242a5fa/sqlalchemy-2.0.49-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4bbccb45260e4ff1b7db0be80a9025bb1e6698bdb808b83fff0000f7a90b2c0b", size = 2157681, upload-time = "2026-04-03T16:53:07.132Z" }, + { url = "https://files.pythonhosted.org/packages/50/84/b2a56e2105bd11ebf9f0b93abddd748e1a78d592819099359aa98134a8bf/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fb37f15714ec2652d574f021d479e78cd4eb9d04396dca36568fdfffb3487982", size = 3338976, upload-time = "2026-04-03T17:07:40Z" }, + { url = "https://files.pythonhosted.org/packages/2c/fa/65fcae2ed62f84ab72cf89536c7c3217a156e71a2c111b1305ab6f0690e2/sqlalchemy-2.0.49-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3bb9ec6436a820a4c006aad1ac351f12de2f2dbdaad171692ee457a02429b672", size = 3351937, upload-time = "2026-04-03T17:12:23.374Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2f/6fd118563572a7fe475925742eb6b3443b2250e346a0cc27d8d408e73773/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8d6efc136f44a7e8bc8088507eaabbb8c2b55b3dbb63fe102c690da0ddebe55e", size = 3281646, upload-time = "2026-04-03T17:07:41.949Z" }, + { url = "https://files.pythonhosted.org/packages/c5/d7/410f4a007c65275b9cf82354adb4bb8ba587b176d0a6ee99caa16fe638f8/sqlalchemy-2.0.49-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e06e617e3d4fd9e51d385dfe45b077a41e9d1b033a7702551e3278ac597dc750", size = 3316695, upload-time = "2026-04-03T17:12:25.642Z" }, + { url = "https://files.pythonhosted.org/packages/d9/95/81f594aa60ded13273a844539041ccf1e66c5a7bed0a8e27810a3b52d522/sqlalchemy-2.0.49-cp312-cp312-win32.whl", hash = "sha256:83101a6930332b87653886c01d1ee7e294b1fe46a07dd9a2d2b4f91bcc88eec0", size = 2117483, upload-time = "2026-04-03T17:05:40.896Z" }, + { url = "https://files.pythonhosted.org/packages/47/9e/fd90114059175cac64e4fafa9bf3ac20584384d66de40793ae2e2f26f3bb/sqlalchemy-2.0.49-cp312-cp312-win_amd64.whl", hash = "sha256:618a308215b6cececb6240b9abde545e3acdabac7ae3e1d4e666896bf5ba44b4", size = 2144494, upload-time = "2026-04-03T17:05:42.282Z" }, + { url = "https://files.pythonhosted.org/packages/ae/81/81755f50eb2478eaf2049728491d4ea4f416c1eb013338682173259efa09/sqlalchemy-2.0.49-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df2d441bacf97022e81ad047e1597552eb3f83ca8a8f1a1fdd43cd7fe3898120", size = 2154547, upload-time = "2026-04-03T16:53:08.64Z" }, + { url = "https://files.pythonhosted.org/packages/a2/bc/3494270da80811d08bcfa247404292428c4fe16294932bce5593f215cad9/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8e20e511dc15265fb433571391ba313e10dd8ea7e509d51686a51313b4ac01a2", size = 3280782, upload-time = "2026-04-03T17:07:43.508Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f5/038741f5e747a5f6ea3e72487211579d8cbea5eb9827a9cbd61d0108c4bd/sqlalchemy-2.0.49-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47604cb2159f8bbd5a1ab48a714557156320f20871ee64d550d8bf2683d980d3", size = 3297156, upload-time = "2026-04-03T17:12:27.697Z" }, + { url = "https://files.pythonhosted.org/packages/88/50/a6af0ff9dc954b43a65ca9b5367334e45d99684c90a3d3413fc19a02d43c/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:22d8798819f86720bc646ab015baff5ea4c971d68121cb36e2ebc2ee43ead2b7", size = 3228832, upload-time = "2026-04-03T17:07:45.38Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d1/5f6bdad8de0bf546fc74370939621396515e0cdb9067402d6ba1b8afbe9a/sqlalchemy-2.0.49-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9b1c058c171b739e7c330760044803099c7fff11511e3ab3573e5327116a9c33", size = 3267000, upload-time = "2026-04-03T17:12:29.657Z" }, + { url = "https://files.pythonhosted.org/packages/f7/30/ad62227b4a9819a5e1c6abff77c0f614fa7c9326e5a3bdbee90f7139382b/sqlalchemy-2.0.49-cp313-cp313-win32.whl", hash = "sha256:a143af2ea6672f2af3f44ed8f9cd020e9cc34c56f0e8db12019d5d9ecf41cb3b", size = 2115641, upload-time = "2026-04-03T17:05:43.989Z" }, + { url = "https://files.pythonhosted.org/packages/17/3a/7215b1b7d6d49dc9a87211be44562077f5f04f9bb5a59552c1c8e2d98173/sqlalchemy-2.0.49-cp313-cp313-win_amd64.whl", hash = "sha256:12b04d1db2663b421fe072d638a138460a51d5a862403295671c4f3987fb9148", size = 2141498, upload-time = "2026-04-03T17:05:45.7Z" }, + { url = "https://files.pythonhosted.org/packages/28/4b/52a0cb2687a9cd1648252bb257be5a1ba2c2ded20ba695c65756a55a15a4/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:24bd94bb301ec672d8f0623eba9226cc90d775d25a0c92b5f8e4965d7f3a1518", size = 3560807, upload-time = "2026-04-03T16:58:31.666Z" }, + { url = "https://files.pythonhosted.org/packages/8c/d8/fda95459204877eed0458550d6c7c64c98cc50c2d8d618026737de9ed41a/sqlalchemy-2.0.49-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a51d3db74ba489266ef55c7a4534eb0b8db9a326553df481c11e5d7660c8364d", size = 3527481, upload-time = "2026-04-03T17:06:00.155Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0a/2aac8b78ac6487240cf7afef8f203ca783e8796002dc0cf65c4ee99ff8bb/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:55250fe61d6ebfd6934a272ee16ef1244e0f16b7af6cd18ab5b1fc9f08631db0", size = 3468565, upload-time = "2026-04-03T16:58:33.414Z" }, + { url = "https://files.pythonhosted.org/packages/a5/3d/ce71cfa82c50a373fd2148b3c870be05027155ce791dc9a5dcf439790b8b/sqlalchemy-2.0.49-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:46796877b47034b559a593d7e4b549aba151dae73f9e78212a3478161c12ab08", size = 3477769, upload-time = "2026-04-03T17:06:02.787Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/0a9f5c1f7c6f9ca480319bf57c2d7423f08d31445974167a27d14483c948/sqlalchemy-2.0.49-cp313-cp313t-win32.whl", hash = "sha256:9c4969a86e41454f2858256c39bdfb966a20961e9b58bf8749b65abf447e9a8d", size = 2143319, upload-time = "2026-04-03T17:02:04.328Z" }, + { url = "https://files.pythonhosted.org/packages/0e/51/fb5240729fbec73006e137c4f7a7918ffd583ab08921e6ff81a999d6517a/sqlalchemy-2.0.49-cp313-cp313t-win_amd64.whl", hash = "sha256:b9870d15ef00e4d0559ae10ee5bc71b654d1f20076dbe8bc7ed19b4c0625ceba", size = 2175104, upload-time = "2026-04-03T17:02:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/33/bf28f618c0a9597d14e0b9ee7d1e0622faff738d44fe986ee287cdf1b8d0/sqlalchemy-2.0.49-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:233088b4b99ebcbc5258c755a097aa52fbf90727a03a5a80781c4b9c54347a2e", size = 2156356, upload-time = "2026-04-03T16:53:09.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a7/5f476227576cb8644650eff68cc35fa837d3802b997465c96b8340ced1e2/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57ca426a48eb2c682dae8204cd89ea8ab7031e2675120a47924fabc7caacbc2a", size = 3276486, upload-time = "2026-04-03T17:07:46.9Z" }, + { url = "https://files.pythonhosted.org/packages/2e/84/efc7c0bf3a1c5eef81d397f6fddac855becdbb11cb38ff957888603014a7/sqlalchemy-2.0.49-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:685e93e9c8f399b0c96a624799820176312f5ceef958c0f88215af4013d29066", size = 3281479, upload-time = "2026-04-03T17:12:32.226Z" }, + { url = "https://files.pythonhosted.org/packages/91/68/bb406fa4257099c67bd75f3f2261b129c63204b9155de0d450b37f004698/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9e0400fa22f79acc334d9a6b185dc00a44a8e6578aa7e12d0ddcd8434152b187", size = 3226269, upload-time = "2026-04-03T17:07:48.678Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/acb56c00cca9f251f437cb49e718e14f7687505749ea9255d7bd8158a6df/sqlalchemy-2.0.49-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a05977bffe9bffd2229f477fa75eabe3192b1b05f408961d1bebff8d1cd4d401", size = 3248260, upload-time = "2026-04-03T17:12:34.381Z" }, + { url = "https://files.pythonhosted.org/packages/56/19/6a20ea25606d1efd7bd1862149bb2a22d1451c3f851d23d887969201633f/sqlalchemy-2.0.49-cp314-cp314-win32.whl", hash = "sha256:0f2fa354ba106eafff2c14b0cc51f22801d1e8b2e4149342023bd6f0955de5f5", size = 2118463, upload-time = "2026-04-03T17:05:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4f/8297e4ed88e80baa1f5aa3c484a0ee29ef3c69c7582f206c916973b75057/sqlalchemy-2.0.49-cp314-cp314-win_amd64.whl", hash = "sha256:77641d299179c37b89cf2343ca9972c88bb6eef0d5fc504a2f86afd15cd5adf5", size = 2144204, upload-time = "2026-04-03T17:05:48.694Z" }, + { url = "https://files.pythonhosted.org/packages/1f/33/95e7216df810c706e0cd3655a778604bbd319ed4f43333127d465a46862d/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c1dc3368794d522f43914e03312202523cc89692f5389c32bea0233924f8d977", size = 3565474, upload-time = "2026-04-03T16:58:35.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/a4/ed7b18d8ccf7f954a83af6bb73866f5bc6f5636f44c7731fbb741f72cc4f/sqlalchemy-2.0.49-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c821c47ecfe05cc32140dcf8dc6fd5d21971c86dbd56eabfe5ba07a64910c01", size = 3530567, upload-time = "2026-04-03T17:06:04.587Z" }, + { url = "https://files.pythonhosted.org/packages/73/a3/20faa869c7e21a827c4a2a42b41353a54b0f9f5e96df5087629c306df71e/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9c04bff9a5335eb95c6ecf1c117576a0aa560def274876fd156cfe5510fccc61", size = 3474282, upload-time = "2026-04-03T16:58:37.131Z" }, + { url = "https://files.pythonhosted.org/packages/b7/50/276b9a007aa0764304ad467eceb70b04822dc32092492ee5f322d559a4dc/sqlalchemy-2.0.49-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7f605a456948c35260e7b2a39f8952a26f077fd25653c37740ed186b90aaa68a", size = 3480406, upload-time = "2026-04-03T17:06:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e5/c3/c80fcdb41905a2df650c2a3e0337198b6848876e63d66fe9188ef9003d24/sqlalchemy-2.0.49-cp314-cp314t-win32.whl", hash = "sha256:6270d717b11c5476b0cbb21eedc8d4dbb7d1a956fd6c15a23e96f197a6193158", size = 2149151, upload-time = "2026-04-03T17:02:07.281Z" }, + { url = "https://files.pythonhosted.org/packages/05/52/9f1a62feab6ed368aff068524ff414f26a6daebc7361861035ae00b05530/sqlalchemy-2.0.49-cp314-cp314t-win_amd64.whl", hash = "sha256:275424295f4256fd301744b8f335cff367825d270f155d522b30c7bf49903ee7", size = 2184178, upload-time = "2026-04-03T17:02:08.623Z" }, + { url = "https://files.pythonhosted.org/packages/e5/30/8519fdde58a7bdf155b714359791ad1dc018b47d60269d5d160d311fdc36/sqlalchemy-2.0.49-py3-none-any.whl", hash = "sha256:ec44cfa7ef1a728e88ad41674de50f6db8cfdb3e2af84af86e0041aaf02d43d0", size = 1942158, upload-time = "2026-04-03T16:53:44.135Z" }, +] + +[package.optional-dependencies] +asyncio = [ + { name = "greenlet" }, +] + [[package]] name = "starlette" version = "0.50.0" diff --git a/example/singleline-app/app.py b/example/singleline-app/app.py new file mode 100644 index 00000000..a412bfd2 --- /dev/null +++ b/example/singleline-app/app.py @@ -0,0 +1,8 @@ +from fastapi_startkit import Application +from fastapi_startkit.fastapi import FastAPIProvider + +app: Application = Application(providers=[FastAPIProvider]) + +@app.get("/") +async def index(): + return {"message": "Hello, World!"} diff --git a/fastapi_startkit/CLAUDE.md b/fastapi_startkit/CLAUDE.md deleted file mode 100644 index 337b4405..00000000 --- a/fastapi_startkit/CLAUDE.md +++ /dev/null @@ -1,174 +0,0 @@ -# CLAUDE.md - -This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. - -## Important - -**Do not modify framework code unless explicitly necessary.** This is a foundational framework used by downstream applications. Changes to core abstractions (Container, Application, Model, Provider, Facades) can have broad breaking effects. - -## Commands - -```bash -# Install dependencies -uv sync - -# Build -uv build - -# Run all tests -uv run pytest src/fastapi_startkit/tests/ -v - -# Run a single test file -uv run pytest src/fastapi_startkit/tests/configurations/test_config_merge.py -v - -# Run a single test function -uv run pytest src/fastapi_startkit/tests/configurations/test_config_merge.py::TestConfiguration::test_merge_with_dict -v -``` - -Tests run with `asyncio_mode = "auto"` (configured in `pyproject.toml`), so all tests are async-capable by default. - -## Architecture - -This is a **FastAPI application framework** providing IoC, configuration, ORM, logging, and CLI tooling. It wraps FastAPI and adds a Laravel/Masonite-inspired structure. - -### Application Lifecycle - -1. `Application(base_path)` initializes the service container and singleton -2. `.load_environment()` loads `.env` + `.env.{APP_ENV}` (auto-detects `.env.testing` under pytest) -3. `.configure_paths()` sets config/storage paths -4. `.register_providers()` → `.load_providers()` (two-phase boot) -5. `app.fastapi` is lazy-loaded; HTTP routes delegate to the FastAPI instance - -### Service Container (`container/container.py`) - -Central IoC container. Core API: -- `bind(key, value)` — register a binding -- `make(key)` — resolve a binding -- `resolve(obj)` — auto-wire a callable by inspecting its type-hinted parameters - -Hooks (`on_bind`, `on_make`, `on_resolve`) allow intercepting container operations. `collect('Auth*')` returns all bindings matching a wildcard. - -### Configuration (`configuration/`) - -#### 1. Defining config with dataclasses - -The recommended approach is to define config as a dataclass, with each field sourced from an environment variable via `env()`: - -```python -from dataclasses import dataclass, field -from fastapi_startkit.environment import env - -@dataclass -class RedisConfig: - host: str = field(default_factory=lambda: env('REDIS_HOST')) - port: int = field(default_factory=lambda: env('REDIS_PORT')) - db: int = field(default_factory=lambda: env('REDIS_DB')) - options: dict = field(default_factory=lambda: { - 'decode_responses': True - }) -``` - -`env()` reads from the currently loaded environment, so calling `RedisConfig()` before and after `app.load_environment()` will produce different values. - -#### 2. Accessing config anywhere - -Because each field is a `default_factory`, instantiating the dataclass at any point will reflect the current environment: - -```python -RedisConfig().host # reads REDIS_HOST from the active .env -RedisConfig().port # reads REDIS_PORT -RedisConfig().options # static default dict -``` - -No injection or container lookup is required for simple access. - -#### 3. Environment-specific `.env` loading - -`app.load_environment()` applies a two-step merge: - -1. Loads `.env` as the base. -2. If an environment is set (e.g. `production`), loads `.env.production` on top, overriding matching keys. - -``` -.env ← always loaded first (base/defaults) -.env.testing ← loaded when APP_ENV=testing (or under pytest) -.env.production ← loaded when APP_ENV=production -``` - -Set the environment in code before loading: - -```python -app.set_environment('testing') -app.load_environment() -``` - -Example — `.env` has `REDIS_HOST=host.default`; `.env.testing` has `REDIS_HOST=host.testing`. After `load_environment()`, `RedisConfig().host` returns `host.testing`. - -#### 4. Setting the environment via the CLI (`artisan`) - -Prefer passing `--env` on the command line over hardcoding it: - -```bash -uv run artisan --env=production # loads .env + .env.production -uv run artisan --env=testing # loads .env + .env.testing -uv run artisan # loads .env only -``` - -The `artisan` entry point at the project root bootstraps the application and delegates to `app.handle_command()`. - -#### 5. Registering config in the container (optional) - -For runtime overrides or dotted-key access to nested values, register a config instance with the container: - -```python -config = app.make('config') -config.set('redis', RedisConfig()) -``` - -Then access it from anywhere via the `Config` facade: - -```python -from fastapi_startkit.facades import Config - -Config.get('redis.host') # 'host.testing' -Config.get('redis.options') # {'decode_responses': True} -``` - -This is most useful when you need to change config at runtime or share nested config across services. Direct instantiation (`RedisConfig().host`) is simpler for read-only access. - -`Configuration.merge_with()` allows packages to inject their own config defaults. - -### Provider Pattern (`providers/`) - -Providers are the standard way to register services. Each provider has two phases: -- `register()` — bind things into the container -- `boot()` — run after all providers are registered (safe to resolve dependencies here) - -### ORM (`masoniteorm/`) - -An async-first fork of Masonite ORM built on SQLAlchemy async. Key points: -- All DB operations are `async`/`await` -- `Model` base class auto-pluralizes table names via `inflection` -- `created_at`/`updated_at` are managed automatically as `pendulum` Carbon objects -- Relationships (`HasOne`, `HasMany`, `BelongsTo`, `BelongsToMany`, `HasOneThrough`) are defined as class attributes -- `AsyncQueryBuilder` provides the chainable query interface - -### Facades (`facades/`) - -Static-like access to container-resolved services (e.g., `Config.get()`, `Auth.user()`). Each facade has a corresponding `.pyi` stub file for IDE type support. Facades resolve from the Application singleton — they require a booted Application to function. - -### Console (`console.py`, `commands/`, `masoniteorm/commands/`) - -CLI is built on [Cleo](https://github.com/python-poetry/cleo). `ConsoleApplication` wraps Cleo and auto-registers commands. Database commands (migrate, seed, make:model, etc.) live in `masoniteorm/commands/`. - -## Key Dependencies - -| Package | Purpose | -|---|---| -| `fastapi[standard]` | HTTP framework (lazily imported) | -| `sqlalchemy[asyncio]` | Async ORM backend | -| `pendulum` | Datetime/timezone (used as Carbon) | -| `cleo` | CLI commands | -| `dotty-dict` | Nested dict access via dotted keys | -| `inflection` | Table name pluralization | -| `asyncpg` / `aiomysql` / `aiosqlite` | DB drivers |