diff --git a/README.md b/README.md index 44ee001..0963c0b 100644 --- a/README.md +++ b/README.md @@ -1,401 +1,40 @@ # OhMyAPI -> Think: Django RestFramework, but less clunky and 100% async. +OhMyAPI is a web-application scaffolding framework and management layer built around `FastAPI`, `TortoiseORM` and `Aerich` migrations. -OhMyAPI is a Django-flavored web-application scaffolding framework and management layer, -built around FastAPI and TortoiseORM and is thus 100% async. +> *Think: Django RestFramework, but less clunky and 100% async.* It is ***blazingly fast***, extremely ***fun to use*** and comes with ***batteries included***! **Features** - Django-like project structure and application directories -- Django-like per-app migrations (`makemigrations` & `migrate`) via Aerich -- Django-like CLI tooling (`startproject`, `startapp`, `shell`, `serve`, etc) -- Customizable pydantic model serializer built-in +- Django-like per-app migrations (makemigrations & migrate) via Aerich +- Django-like CLI tooling (startproject, startapp, shell, serve, etc) +- Customizable pydantic model serializer built-in - Various optional built-in apps you can hook into your project - Highly configurable and customizable - 100% async **Goals** -- combine `FastAPI`, `TortoiseORM`, `Aerich` migrations and `Pydantic` into a high-productivity web-application framework +- combine FastAPI, TortoiseORM, Aerich migrations and Pydantic into a high-productivity web-application framework - tie everything neatly together into a concise and straight-forward API -- ***AVOID*** adding any abstractions on top, unless they make things extremely convenient +- AVOID adding any abstractions on top, unless they make things extremely convenient ---- - -## Getting started - -**Creating a Project** +## Installation ``` pipx install ohmyapi -ohmyapi startproject myproject -cd myproject ``` -This will create the following directory structure: -``` -myproject/ - - pyproject.toml - - README.md - - settings.py -``` +## Docs -Run your project with: - -``` -ohmyapi serve -``` - -In your browser go to: -- http://localhost:8000/docs - -**Creating an App** - -Create a new app by: - -``` -ohmyapi startapp tournament -``` - -This will create the following directory structure: - -``` -myproject/ - - tournament/ - - __init__.py - - models.py - - routes.py - - pyproject.toml - - README.md - - settings.py -``` - -Add 'tournament' to your `INSTALLED_APPS` in `settings.py`. - -### Models - -Write your first model in `turnament/models.py`: - -```python -from ohmyapi.db import Model, field - -from datetime import datetime -from decimal import Decimal -from uuid import UUID - - -class Tournament(Model): - id: UUID = field.data.UUIDField(primary_key=True) - name: str = field.TextField() - created: datetime = field.DatetimeField(auto_now_add=True) - - def __str__(self): - return self.name - - -class Event(Model): - id: UUID = field.data.UUIDField(primary_key=True) - name: str = field.TextField() - tournament: UUID = field.ForeignKeyField('tournament.Tournament', related_name='events') - participants: field.ManyToManyRelation[Team] = field.ManyToManyField('tournament.Team', related_name='events', through='event_team') - modified: datetime = field.DatetimeField(auto_now=True) - prize: Decimal = field.DecimalField(max_digits=10, decimal_places=2, null=True) - - def __str__(self): - return self.name - - -class Team(Model): - id: UUID = field.data.UUIDField(primary_key=True) - name: str = field.TextField() - - def __str__(self): - return self.name -``` - -### API Routes - -Next, create your endpoints in `tournament/routes.py`: - -```python -from ohmyapi.router import APIRouter, HTTPException, HTTPStatus -from ohmyapi.db.exceptions import DoesNotExist - -from typing import List - -from .models import Tournament - -# OhMyAPI will automatically pick up all instances of `fastapi.APIRouter` and -# add their routes to the main project router. -# -# Note: -# Use prefixes wisely to avoid cross-app namespace-collisions! -# Tags improve the UX of the OpenAPI docs at /docs. -# -tournament_router = APIRouter(prefix="/tournament", tags=['Tournament']) - - -@tournament_router.get("/", response_model=List[Tournament.Schema()]) -async def list(): - queryset = Tournament.all() - return await Tournament.Schema.model.from_queryset(queryset) - - -@tournament_router.post("/", status_code=HTTPStatus.CREATED) -async def post(tournament: Tournament.Schema(readonly=True)): - queryset = Tournament.create(**payload.model_dump()) - return await Tournament.Schema().from_queryset(queryset) - - -@tournament_router.get("/:id", response_model=Tournament.Schema()) -async def get(id: str): - try: - queryset = Tournament.get(id=id) - return await Tournament.Schema().from_queryset_single(tournament) - except DoesNotExist: - raise HTTPException(status_code=404, detail="not found") - - -@tournament_router.delete("/:id") -async def delete(id: str): - try: - tournament = await Tournament.get(id=id) - return await Tournament.Schema.model.from_queryset(tournament.delete()) - except DoesNotExist: - raise HTTPException(status_code=404, detail="not found") - - -... -``` - -## Migrations - -Before we can run the app, we need to create and initialize the database. - -Similar to Django, first run: - -``` -ohmyapi makemigrations [ ] # no app means all INSTALLED_APPS -``` - -This will create a `migrations/` folder in you project root. - -``` -myproject/ - - tournament/ - - __init__.py - - models.py - - routes.py - - migrations/ - - tournament/ - - pyproject.toml - - README.md - - settings.py -``` - -Apply your migrations via: - -``` -ohmyapi migrate [ ] # no app means all INSTALLED_APPS -``` - -Run your project: - -``` -ohmyapi serve -``` - -## Authentication - -A builtin auth app is available. - -Simply add `ohmyapi_auth` to your INSTALLED_APPS and define a JWT_SECRET in your `settings.py`. -Remember to `makemigrations` and `migrate` for the necessary tables to be created in the database. - -`settings.py`: - -``` -INSTALLED_APPS = [ - 'ohmyapi_auth', - ... -] - -JWT_SECRET = "t0ps3cr3t" -``` - -After restarting your project you will have access to the `ohmyapi_auth` app. -It comes with a `User` and `Group` model, as well as endpoints for JWT auth. - -You can use the models as `ForeignKeyField` in your application models: - -```python -from ohmyapi.db import Model, field -from ohmyapi_auth.models import User - - -class Team(Model): - [...] - members: field.ManyToManyRelation[User] = field.ManyToManyField('ohmyapi_auth.User', related_name='tournament_teams', through='tournament_teams') - [...] -``` - -Remember to run `makemigrations` and `migrate` in order for your model changes to take effect in the database. - -Create a super-user: - -``` -ohmyapi createsuperuser -``` - -## Permissions - -### API-Level Permissions - -Use FastAPI's `Depends` pattern to implement API-level access-control. - - -In your `routes.py`: - -```python -from ohmyapi.router import APIRouter, Depends -from ohmyapi_auth import ( - models as auth, - permissions, -) - -from .models import Tournament - -router = APIRouter(prefix="/tournament", tags=["Tournament"]) - - -@router.get("/") -async def list(user: auth.User = Depends(permissions.require_authenticated)): - queryset = Tournament.all() - return await Tournament.Schema().from_queryset(queryset) - - -... -``` - -### Model-Level Permissions - -Use Tortoise's `Manager` to implement model-level permissions. - -```python -from ohmyapi.db import Manager -from ohmyapi_auth.models import User - - -class TeamManager(Manager): - async def for_user(self, user: User): - return await self.filter(members=user).all() - - -class Team(Model): - [...] - - class Meta: - manager = TeamManager() -``` - -Use the custom manager in your FastAPI route handler: - -```python -from ohmyapi.router import APIRouter -from ohmyapi_auth import ( - models as auth, - permissions, -) - -router = APIRouter(prefix="/tournament", tags=["Tournament"]) - - -@router.get("/teams") -async def teams(user: auth.User = Depends(permissions.require_authenticated)): - queryset = Team.for_user(user) - return await Tournament.Schema().from_queryset(queryset) -``` - -## Shell - -Similar to Django, you can attach to an interactive shell with your project already loaded inside. - -``` -ohmyapi shell - -Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813] -Type 'copyright', 'credits' or 'license' for more information -IPython 9.5.0 -- An enhanced Interactive Python. Type '?' for help. - -OhMyAPI Shell | Project: {{ project_name }} [{{ project_path }}] -Find your loaded project singleton via identifier: `p` -``` - -```python -In [1]: p -Out[1]: - -In [2]: p.apps -Out[2]: -{'ohmyapi_auth': { - "models": [ - "Group", - "User" - ], - "routes": [ - { - "path": "/auth/login", - "name": "login", - "methods": [ - "POST" - ], - "endpoint": "login", - "response_model": null, - "tags": [ - "auth" - ] - }, - { - "path": "/auth/refresh", - "name": "refresh_token", - "methods": [ - "POST" - ], - "endpoint": "refresh_token", - "response_model": null, - "tags": [ - "auth" - ] - }, - { - "path": "/auth/introspect", - "name": "introspect", - "methods": [ - "GET" - ], - "endpoint": "introspect", - "response_model": null, - "tags": [ - "auth" - ] - }, - { - "path": "/auth/me", - "name": "me", - "methods": [ - "GET" - ], - "endpoint": "me", - "response_model": null, - "tags": [ - "auth" - ] - } - ] - }} -``` +See: `docs/` +- [Projects](docs/projects.md) +- [Apps](docs/apps.md) +- [Models](docs/models.md) +- [Migrations](docs/migrations.md) +- [Routes](docs/routes.md) diff --git a/docs/apps.md b/docs/apps.md new file mode 100644 index 0000000..fdcf5c4 --- /dev/null +++ b/docs/apps.md @@ -0,0 +1,34 @@ +# Apps + +Apps are a way to group database models and API routes that contextually belong together. +For example, OhMyAPI comes bundled with an `auth` app that carries a `User` and `Group` model and provides API endpoints for JWT authentication. + +Apps help organizing projects by isolating individual components (or "features") from one another. + +## Create an App + +Create a new app by: `ohmyapi startapp `, i.e.: + +``` +ohmyapi startapp restaurant +``` + +This will create the following directory structure: + +``` +myproject/ + - restaurant/ + - __init__.py + - models.py + - routes.py + - pyproject.toml + - README.md + - settings.py +``` + +Add `restaurant` to your `INSTALLED_APPS` in `settings.py` and restart you project runtime: + +``` +ohmyapi serve +``` + diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..453e6d7 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,30 @@ +# Welcome to OhMyAPI + +OhMyAPI is a web-application scaffolding framework and management layer built around `FastAPI`, `TortoiseORM` and `Aerich` migrations. + +> *Think: Django RestFramework, but less clunky and 100% async.* + +It is ***blazingly fast***, extremely ***fun to use*** and comes with ***batteries included***! + +**Features** + +- Django-like project structure and application directories +- Django-like per-app migrations (makemigrations & migrate) via Aerich +- Django-like CLI tooling (startproject, startapp, shell, serve, etc) +- Customizable pydantic model serializer built-in +- Various optional built-in apps you can hook into your project +- Highly configurable and customizable +- 100% async + +**Goals** + +- combine FastAPI, TortoiseORM, Aerich migrations and Pydantic into a high-productivity web-application framework +- tie everything neatly together into a concise and straight-forward API +- AVOID adding any abstractions on top, unless they make things extremely convenient + +## Installation + +``` +pipx install ohmyapi +``` + diff --git a/docs/migrations.md b/docs/migrations.md new file mode 100644 index 0000000..e8052e6 --- /dev/null +++ b/docs/migrations.md @@ -0,0 +1,22 @@ +# Migrations + +OhMyAPI uses [Aerich](https://github.com/tortoise/aerich) - a database migrations tool for TortoiseORM. + +## Making migrations + +Whenever you add, remove or change fields of a database model, you need to create a migration for the change. + +``` +ohmyapi makemigrations [ ] # no app indicates all INSTALLED_APPS +``` + +This will create a `migrations/` directory with subdirectories for each of your apps. + +## Migrating + +When the migrations are create, they need to be applied: + +``` +ohmyapi migrate [ ] # no app indicates all INSTALLED_APPS +``` + diff --git a/docs/models.md b/docs/models.md new file mode 100644 index 0000000..47b5708 --- /dev/null +++ b/docs/models.md @@ -0,0 +1,102 @@ +# Models + +OhMyAPI uses [Tortoise](https://tortoise.github.io/) - an easy-to-use asyncio ORM (Object Relational Mapper) inspired by Django. + +Models are exposed via a Python module named `models` in the app's directory. +OhMyAPI auto-detects all models exposed this way. +If the `models` module is a package, OhMyAPI will search through its submodules recursively. + +## Writing models + +### Your first simple model + +```python +from ohmyapi.db import Model, field + + +class Restaurant(Model): + id: int = field.IntField(pk=True) + name: str = field.CharField(max_length=255) + description: str = field.TextField() + location: str = field.CharField(max_length=255) +``` + +### ForeignKeyRelations + +You can define relationships between models. + +```python +from ohmyapi.db import Model, field + +from decimal import Decimal + + +class Restaurant(Model): + id: int = field.IntField(pk=True) + name: str = field.CharField(max_length=255) + description: str = field.TextField() + location: str = field.CharField(max_length=255) + + +class Dish(Model): + id: int = field.IntField(pk=True) + restaurant: field.ForeignKeyRelation[Restaurant] = field.ForeignKeyField( + "restaurant.Restaurant", + related_name="dishes", + ) + name: str = field.CharField(max_length=255) + price: Decimal = field.DecimalField(max_digits=10, decimal_places=2) + + +class Order(Model): + id: int = field.IntField(pk=True) + restaurant: field.ForeignKeyRelation[Restaurant] = field.ForeignKeyField( + 'restaurant.Restaurant', + related_name="dishes", + ) + dishes: field.ManyToManyRelation[Dish] = field.ManyToManyField( + "restaurant.Dish", + relatated_name="orders", + through="dishesordered", + ) +``` + +## Pydantic Schema Validation + +Each model has a builtin `Schema` class that provides easy access to Pydantic models for schema validation. +Each model provides a default schema and a readonly schema, which you can obtain via the `get` method or by calling Schema() directly. +The default schema contains the full collection of fields of the Tortoise model. +The readonly schema excludes the primary-key field, as well as all readonly fields. + +```python +In [1]: from restaurant.models import Restaurant + +In [2]: Restaurant.Schema.get() +Out[2]: tortoise.contrib.pydantic.creator.RestaurantSchema + +In [3]: Restaurant.Schema.get(readonly=True) +Out[3]: tortoise.contrib.pydantic.creator.RestaurantSchemaReadonly + +In [4]: data = { + ...: "name": "My Pizzeria", + ...: "description": "Awesome Pizza!", + ...: "location": "Berlin", + ...: } + +In [5]: Restaurant.Schema.get(readonly=True)(**data) +Out[5]: RestaurantSchemaReadonly(name='My Pizzeria', description='Awesome Pizza!', location='Berlin') + +In [6]: Restaurant(**_.model_dump()) +Out[6]: +``` + +You can customize the fields to be include in the Pydantic schema: + +```python +class MyModel(Model): + [...] + + class Schema: + include: List[str] = [] # list of fields to include + exclude: List[str] = [] # list of fields to exclude +``` diff --git a/docs/projects.md b/docs/projects.md new file mode 100644 index 0000000..7fbcdf1 --- /dev/null +++ b/docs/projects.md @@ -0,0 +1,34 @@ +# Projects + +OhMyAPI organizes projects in a diretory tree. +The root directory contains the `settings.py`, which carries global configuration for your project, such as your `DATABASE_URL` and `INSTALLED_APPS`. +Each project is organized into individual apps, which in turn may provide some database models and API handlers. +Each app is isolated in its own subdirectory within your project. +You can control which apps to install and load via `INSTALLED_APPS` in your `settings.py`. + +## Create a Project + +To create a projects, simply run: + +``` +ohmyapi startproject myproject +cd myproject +``` + +This will create the following directory structure: + +``` +myproject/ + - pyproject.toml + - README.md + - settings.py +``` + +Run your project with: + +``` +ohmyapi serve +``` + +In your browser go to: [http://localhost:8000/docs](http://localhost:8000/docs) + diff --git a/docs/routes.md b/docs/routes.md new file mode 100644 index 0000000..185f219 --- /dev/null +++ b/docs/routes.md @@ -0,0 +1,68 @@ +# Routes + +OhMyAPI uses [FastAPI](https://fastapi.tiangolo.com/) - a modern, fast (high-performance), web framework for building APIs with Python based on standard Python type hints. + +Routes are exposed via a module named `routes` in the app's directory. +OhMyAPI auto-detects all `fastapi.APIRouter` instances exposed this way. +If the `routes` module is a package, OhMyAPI will search through its submodules recursively. + +When creating an app via `startapp`, OhMyAPI will provide CRUD boilerplate to help your get started. + +## Example CRUD API endpoints + +```python +from ohmyapi.db.exceptions import DoesNotExist + +from ohmyapi.router import APIRouter, HTTPException, HTTPStatus + +from .models import Restaurant + +from typing import List + +router = APIRouter(prefix="/restaurant", tags=['restaurant']) + + +@router.get("/", response_model=List[Restaurant]) +async def list(): + """List all restaurants.""" + queryset = Restaurant.all() + schema = Restaurant.Schema() + return await schema.from_queryset(queryset) + # or in one line: + # return await Restaurant.Schema().from_queryset(Restaurant.all()) + + +@router.post("/", status_code=HTTPStatus.CREATED) +async def post(restaurant: Restaurant.Schema(readonly=True)): + """Create a new restaurant.""" + return await Restaurant(**restaurant.model_dump()).create() + + +@router.get("/{id}", response_model=Restaurant) +async def get(id: str): + """Get restaurant by ID.""" + return await Restaurant.Schema().from_queryset(Restaurant.get(id=id)) + + +@router.put("/{id}", status_code=HTTPStatus.ACCEPTED) +async def put(restaurant: Restaurant): + """Update restaurant.""" + try: + db_restaurant = await Restaurant.get(id=id) + except DoesNotExist: + return HTTPException(status_code=HTTPStatus.NOT_FOUND) + + db_restaurant.update_from_dict(restaurant.model_dump()) + return await db_restaurant.save() + + +@router.delete("/{id}", status_code=HTTPStatus.ACCEPTED) +async def delete(id: str): + try: + db_restaurant = await Restaurant.get(id=id) + except DoesNotExist: + return HTTPException(status_code=HTTPStatus.NOT_FOUND) + + return await db_restaurant.delete() + +``` diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..c634233 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,9 @@ +site_name: OhMyAPI Docs +theme: readthedocs +nav: + - Home: index.md + - Projects: projects.md + - Apps: apps.md + - Models: models.md + - Migrations: migrations.md + - Routes: routes.md diff --git a/poetry.lock b/poetry.lock index 8ccdf2e..a07655e 100644 --- a/poetry.lock +++ b/poetry.lock @@ -519,6 +519,24 @@ all = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (> standard = ["email-validator (>=2.0.0)", "fastapi-cli[standard] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] standard-no-fastapi-cloud-cli = ["email-validator (>=2.0.0)", "fastapi-cli[standard-no-fastapi-cloud-cli] (>=0.0.8)", "httpx (>=0.23.0,<1.0.0)", "jinja2 (>=3.1.5)", "python-multipart (>=0.0.18)", "uvicorn[standard] (>=0.12.0)"] +[[package]] +name = "ghp-import" +version = "2.1.0" +description = "Copy your docs directly to the gh-pages branch." +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343"}, + {file = "ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619"}, +] + +[package.dependencies] +python-dateutil = ">=2.8.1" + +[package.extras] +dev = ["flake8", "markdown", "twine", "wheel"] + [[package]] name = "h11" version = "0.16.0" @@ -649,7 +667,7 @@ version = "3.1.6" description = "A very fast and expressive template engine." optional = false python-versions = ">=3.7" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, @@ -661,6 +679,22 @@ MarkupSafe = ">=2.0" [package.extras] i18n = ["Babel (>=2.7)"] +[[package]] +name = "markdown" +version = "3.9" +description = "Python implementation of John Gruber's Markdown." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "markdown-3.9-py3-none-any.whl", hash = "sha256:9f4d91ed810864ea88a6f32c07ba8bee1346c0cc1f6b1f9f6c822f2a9667d280"}, + {file = "markdown-3.9.tar.gz", hash = "sha256:d2900fe1782bd33bdbbd56859defef70c2e78fc46668f8eb9df3128138f2cb6a"}, +] + +[package.extras] +docs = ["mdx_gh_links (>=0.2)", "mkdocs (>=1.6)", "mkdocs-gen-files", "mkdocs-literate-nav", "mkdocs-nature (>=0.6)", "mkdocs-section-index", "mkdocstrings[python]"] +testing = ["coverage", "pyyaml"] + [[package]] name = "markdown-it-py" version = "4.0.0" @@ -691,7 +725,7 @@ version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." optional = false python-versions = ">=3.9" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, @@ -783,6 +817,66 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "mergedeep" +version = "1.3.4" +description = "A deep merge function for 🐍." +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307"}, + {file = "mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8"}, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +description = "Project documentation with Markdown." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e"}, + {file = "mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", markers = "platform_system == \"Windows\""} +ghp-import = ">=1.0" +jinja2 = ">=2.11.1" +markdown = ">=3.3.6" +markupsafe = ">=2.0.1" +mergedeep = ">=1.3.4" +mkdocs-get-deps = ">=0.2.0" +packaging = ">=20.5" +pathspec = ">=0.11.1" +pyyaml = ">=5.1" +pyyaml-env-tag = ">=0.1" +watchdog = ">=2.0" + +[package.extras] +i18n = ["babel (>=2.9.0)"] +min-versions = ["babel (==2.9.0)", "click (==7.0)", "colorama (==0.4) ; platform_system == \"Windows\"", "ghp-import (==1.0)", "importlib-metadata (==4.4) ; python_version < \"3.10\"", "jinja2 (==2.11.1)", "markdown (==3.3.6)", "markupsafe (==2.0.1)", "mergedeep (==1.3.4)", "mkdocs-get-deps (==0.2.0)", "packaging (==20.5)", "pathspec (==0.11.1)", "pyyaml (==5.1)", "pyyaml-env-tag (==0.1)", "watchdog (==2.0)"] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +description = "MkDocs extension that lists all dependencies according to a mkdocs.yml file" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134"}, + {file = "mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c"}, +] + +[package.dependencies] +mergedeep = ">=1.3.4" +platformdirs = ">=2.2.0" +pyyaml = ">=5.1" + [[package]] name = "mypy-extensions" version = "1.1.0" @@ -1138,6 +1232,21 @@ files = [ {file = "pypika_tortoise-0.6.2.tar.gz", hash = "sha256:f95ab59d9b6454db2e8daa0934728458350a1f3d56e81d9d1debc8eebeff26b3"}, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +description = "Extensions to the standard Python datetime module" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3"}, + {file = "python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"}, +] + +[package.dependencies] +six = ">=1.5" + [[package]] name = "python-multipart" version = "0.0.20" @@ -1183,7 +1292,7 @@ version = "6.0.3" description = "YAML parser and emitter for Python" optional = false python-versions = ">=3.8" -groups = ["main"] +groups = ["main", "dev"] files = [ {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, @@ -1253,6 +1362,21 @@ files = [ {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, ] +[[package]] +name = "pyyaml-env-tag" +version = "1.1" +description = "A custom YAML tag for referencing environment variables in YAML files." +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pyyaml_env_tag-1.1-py3-none-any.whl", hash = "sha256:17109e1a528561e32f026364712fee1264bc2ea6715120891174ed1b980d2e04"}, + {file = "pyyaml_env_tag-1.1.tar.gz", hash = "sha256:2eb38b75a2d21ee0475d6d97ec19c63287a7e140231e4214969d0eac923cd7ff"}, +] + +[package.dependencies] +pyyaml = "*" + [[package]] name = "requests" version = "2.32.5" @@ -1318,6 +1442,18 @@ files = [ {file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"}, ] +[[package]] +name = "six" +version = "1.17.0" +description = "Python 2 and 3 compatibility utilities" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" +groups = ["dev"] +files = [ + {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, + {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, +] + [[package]] name = "sniffio" version = "1.3.1" @@ -1494,6 +1630,49 @@ h11 = ">=0.8" [package.extras] standard = ["colorama (>=0.4) ; sys_platform == \"win32\"", "httptools (>=0.6.3)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.15.1) ; sys_platform != \"win32\" and sys_platform != \"cygwin\" and platform_python_implementation != \"PyPy\"", "watchfiles (>=0.13)", "websockets (>=10.4)"] +[[package]] +name = "watchdog" +version = "6.0.0" +description = "Filesystem events monitoring" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112"}, + {file = "watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2"}, + {file = "watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860"}, + {file = "watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134"}, + {file = "watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e6f0e77c9417e7cd62af82529b10563db3423625c5fce018430b249bf977f9e8"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:90c8e78f3b94014f7aaae121e6b909674df5b46ec24d6bebc45c44c56729af2a"}, + {file = "watchdog-6.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e7631a77ffb1f7d2eefa4445ebbee491c720a5661ddf6df3498ebecae5ed375c"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881"}, + {file = "watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7a0e56874cfbc4b9b05c60c8a1926fedf56324bb08cfbc188969777940aef3aa"}, + {file = "watchdog-6.0.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:e6439e374fc012255b4ec786ae3c4bc838cd7309a540e5fe0952d03687d8804e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c"}, + {file = "watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2"}, + {file = "watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a"}, + {file = "watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680"}, + {file = "watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f"}, + {file = "watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282"}, +] + +[package.extras] +watchmedo = ["PyYAML (>=3.10)"] + [[package]] name = "wcwidth" version = "0.2.14" @@ -1512,4 +1691,4 @@ auth = ["argon2-cffi", "crypto", "passlib", "pyjwt", "python-multipart"] [metadata] lock-version = "2.1" python-versions = ">=3.11" -content-hash = "c765d9f42a4d8bee26474bda7e19f0b6fdd43833688d0db781611090e9ee3b99" +content-hash = "3d301460081dada359d425d69feefc63c1e5135aa64b6f000f554bfc1231febd" diff --git a/pyproject.toml b/pyproject.toml index 3574814..c9a7914 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,6 +29,7 @@ dependencies = [ ipython = ">=9.5.0,<10.0.0" black = "^25.9.0" isort = "^6.0.1" +mkdocs = "^1.6.1" [project.optional-dependencies] auth = ["passlib", "pyjwt", "crypto", "argon2-cffi", "python-multipart"]