Compare commits
177 commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b5691f3133 | ||
|
|
5f80a7a86f | ||
|
|
b588ebcf8a | ||
|
|
a9b88d87d6 | ||
|
|
7163fe778e | ||
|
|
458ffc6b2c | ||
|
|
22ca522615 | ||
|
|
8c2cf01f40 | ||
|
|
9d2e284da3 | ||
|
|
ed30291a4c | ||
|
|
31f4da773c | ||
|
|
715b7a030a | ||
|
|
4a5bafd889 | ||
|
|
b50cbc4341 | ||
|
|
10681cc15b | ||
|
|
58f1387aaf | ||
|
|
6b87bfeefb | ||
|
|
812e89ede9 | ||
|
|
e25c9d1715 | ||
|
|
d494396728 | ||
|
|
2e1ec5d780 | ||
|
|
ce47e3f60e | ||
|
|
acd4844a25 | ||
|
|
74f625ab1d | ||
|
|
a45f03b92f | ||
|
|
de043ddd97 | ||
|
|
80163ce994 | ||
|
|
66176e9af7 | ||
|
|
cf106e8855 | ||
|
|
d7f7db338f | ||
|
|
643a6b2eb7 | ||
|
|
1c42b44d41 | ||
|
|
2239480dc0 | ||
|
|
3e15aa7722 | ||
|
|
e1f5ce589c | ||
|
|
4ec2f87ce2 | ||
|
|
e3abc642ed | ||
|
|
b1222c64d6 | ||
|
|
1ef64c2b17 | ||
|
|
49c24b7f0c | ||
|
|
4c11297d65 | ||
|
|
c23576b393 | ||
|
|
0deb5706f8 | ||
|
|
f579972466 | ||
|
|
cb9acd52d0 | ||
|
|
67d89a94f4 | ||
|
|
e848601f79 | ||
|
|
6120c151f1 | ||
|
|
3e029e1fb7 | ||
|
|
9ac95af005 | ||
|
|
33a9c94042 | ||
|
|
cc3872cf74 | ||
|
|
5c632cbe8f | ||
|
|
ad8986abb1 | ||
|
|
bcdd23652f | ||
|
|
856ea12f52 | ||
|
|
bb8884f419 | ||
|
|
63d2c31763 | ||
|
|
64e98f9f0a | ||
|
|
c411a9795c | ||
|
|
e2f968bac4 | ||
|
|
e9d0fb5b80 | ||
|
|
f74b20a19f | ||
|
|
6089f950b6 | ||
|
|
e894c8f100 | ||
|
|
26a072714d | ||
|
|
795dab71f0 | ||
|
|
af110cec9d | ||
|
|
ee4bd2760c | ||
|
|
28f76fc4f4 | ||
|
|
63a4f4f948 | ||
|
|
37d807eb65 | ||
|
|
f0e5c8c30e | ||
|
|
e53c206b4e | ||
|
|
91baf968d7 | ||
|
|
a3d9862c4e | ||
|
|
ed55c3708f | ||
|
|
ed3a776bde | ||
|
|
3de9352227 | ||
|
|
00e18af8fd | ||
|
|
16f15a3d65 | ||
|
|
2232726e7c | ||
|
|
642359bdeb | ||
|
|
4d8952eff7 | ||
|
|
e43dced167 | ||
|
|
7c75cea413 | ||
|
|
bbadd1c132 | ||
|
|
4550549c2c | ||
|
|
2399b28c52 | ||
|
|
c56ea6451e | ||
|
|
29a5018ae3 | ||
|
|
1b830f7bd2 | ||
|
|
e142489ed9 | ||
|
|
4fffeda0ba | ||
|
|
7edd17d359 | ||
|
|
cc2c9a3647 | ||
|
|
737a06c05d | ||
|
|
b07df29c9c | ||
|
|
ff8384d2c5 | ||
|
|
250bf142ed | ||
|
|
61ef27936c | ||
|
|
90f257ae38 | ||
|
|
64d6ca369f | ||
|
|
31dd3a9e37 | ||
|
|
111a65da85 | ||
|
|
c8206547d8 | ||
|
|
3e682bbc89 | ||
|
|
905ce66b1a | ||
|
|
6a90e4a44a | ||
|
|
9becfc857d | ||
|
|
3ebebe7fbd | ||
|
|
80a4b468b1 | ||
|
|
30a7826eeb | ||
|
|
3465ec71c7 | ||
|
|
b15ce0b044 | ||
|
|
82c39540a9 | ||
|
|
09648fa292 | ||
|
|
d509b58282 | ||
|
|
9bf33d12c9 | ||
|
|
ac60c19551 | ||
|
|
1dcbab06b1 | ||
|
|
52297d8ac3 | ||
|
|
adf3fc9ca9 | ||
|
|
eac45bdeb3 | ||
|
|
af1d502570 | ||
|
|
f2f6beb770 | ||
|
|
d67ae5d3f5 | ||
|
|
c43d5030b9 | ||
|
|
db329a8822 | ||
|
|
2870d55926 | ||
|
|
7ca64e8aef | ||
|
|
264119a67d | ||
|
|
33b8ff7acb | ||
|
|
15608a389b | ||
|
|
b6d209926f | ||
|
|
485ddc01fb | ||
|
|
0ec26895c0 | ||
|
|
97fc689d7d | ||
|
|
3071d76ae1 | ||
|
|
20a826e8c0 | ||
|
|
42f7713345 | ||
|
|
a7a792b7b2 | ||
|
|
49faab5be5 | ||
|
|
92bb1cc648 | ||
|
|
b5b005448a | ||
|
|
8543957095 | ||
|
|
1bcd7c0f1f | ||
|
|
0941a9e9d6 | ||
|
|
ffac376dde | ||
|
|
8b4c03a778 | ||
|
|
036e041be7 | ||
|
|
ce6b57bf9d | ||
|
|
aea68b8128 | ||
|
|
35e6ddfcf5 | ||
|
|
5379c125c4 | ||
|
|
c15bc82caa | ||
|
|
812049eae7 | ||
|
|
51037b615a | ||
|
|
018587618e | ||
|
|
73785faebf | ||
|
|
3d61ecd216 | ||
|
|
8f4648643d | ||
|
|
3165243755 | ||
|
|
82fe75b0c7 | ||
|
|
970117a474 | ||
|
|
8d486001b6 | ||
|
|
0baedd94d9 | ||
|
|
091e8a4605 | ||
|
|
df2d2fd89c | ||
|
|
3958c51213 | ||
|
|
7cf7d6ccfc | ||
|
|
2e83e65c7e | ||
|
|
e2669853ae | ||
|
|
5d204d24e8 | ||
|
|
557bda6045 | ||
|
|
69bd447374 | ||
|
|
3c1ae8f5f4 |
41 changed files with 1998 additions and 654 deletions
186
README.md
186
README.md
|
|
@ -1,177 +1,41 @@
|
||||||
# OhMyAPI
|
# OhMyAPI
|
||||||
|
|
||||||
> OhMyAPI == Application scaffolding for FastAPI+TortoiseORM.
|
OhMyAPI is a web-application scaffolding framework and management layer built around `FastAPI`.
|
||||||
|
It is a thin layer that tightly integrates TortoiseORM and Aerich migrations.
|
||||||
|
|
||||||
OhMyAPI is a Django-flavored web-application scaffolding framework.
|
> *Think: *"Django RestFramework"*, but less clunky and instead 100% async.*
|
||||||
Built around FastAPI and TortoiseORM, it 100% async.
|
|
||||||
It is blazingly fast and has batteries included.
|
|
||||||
|
|
||||||
Features:
|
It is ***blazingly fast***, extremely ***fun to use*** and comes with ***batteries included***!
|
||||||
|
|
||||||
- Django-like project-layout and -structure
|
**Features**
|
||||||
- Django-like settings.py
|
|
||||||
- Django-like models via TortoiseORM
|
- Django-like project structure and application directories
|
||||||
- Django-like model.Meta class for model configuration
|
- Django-like per-app migrations (makemigrations & migrate) via Aerich
|
||||||
- Django-like advanced permissions system
|
- Django-like CLI tooling (startproject, startapp, shell, serve, etc)
|
||||||
- Django-like migrations (makemigrations & migrate) via Aerich
|
- Customizable pydantic model serializer built-in
|
||||||
- Django-like CLI for interfacing with your projects (startproject, startapp, shell, serve, etc)
|
- Various optional built-in apps you can hook into your project (i.e. authentication and more)
|
||||||
- various optional builtin apps
|
- Highly configurable and customizable
|
||||||
- highly configurable and customizable
|
|
||||||
- 100% async
|
- 100% async
|
||||||
|
|
||||||
## Getting started
|
**Goals**
|
||||||
|
|
||||||
**Creating a Project**
|
- 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
|
||||||
|
|
||||||
```
|
```
|
||||||
pip install ohmyapi
|
pipx install ohmyapi
|
||||||
ohmyapi startproject myproject
|
|
||||||
cd myproject
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This will create the following directory structure:
|
|
||||||
|
## Docs
|
||||||
|
|
||||||
|
See `docs/` or:
|
||||||
|
|
||||||
```
|
```
|
||||||
myproject/
|
poetry run mkdocs serve
|
||||||
- pyproject.toml
|
|
||||||
- settings.py
|
|
||||||
```
|
|
||||||
|
|
||||||
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 myapp
|
|
||||||
```
|
|
||||||
|
|
||||||
This will lead to the following directory structure:
|
|
||||||
|
|
||||||
```
|
|
||||||
myproject/
|
|
||||||
- myapp/
|
|
||||||
- __init__.py
|
|
||||||
- models.py
|
|
||||||
- routes.py
|
|
||||||
- pyproject.toml
|
|
||||||
- settings.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Add 'myapp' to your `INSTALLED_APPS` in `settings.py`.
|
|
||||||
|
|
||||||
Write your first model in `myapp/models.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from ohmyapi.db import Model, field
|
|
||||||
|
|
||||||
|
|
||||||
class Person(Model):
|
|
||||||
id: int = field.IntField(min=1, pk=True)
|
|
||||||
name: str = field.CharField(min_length=1, max_length=255)
|
|
||||||
username: str = field.CharField(min_length=1, max_length=255, unique=True)
|
|
||||||
age: int = field.IntField(min=0)
|
|
||||||
```
|
|
||||||
|
|
||||||
Next, create your endpoints in `myapp/routes.py`:
|
|
||||||
|
|
||||||
```python
|
|
||||||
from fastapi import APIRouter, HTTPException
|
|
||||||
from tortoise.exceptions import DoesNotExist
|
|
||||||
|
|
||||||
from .models import Person
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/myapp")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
|
||||||
async def list():
|
|
||||||
return await Person.Schema.many.from_queryset(Person.all())
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/:id")
|
|
||||||
async def get(id: int):
|
|
||||||
try:
|
|
||||||
return await Person.Schema.one(Person.get(pk=id))
|
|
||||||
except DoesNotExist:
|
|
||||||
raise HTTPException(status_code=404, detail="item not found")
|
|
||||||
|
|
||||||
...
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migrations
|
|
||||||
|
|
||||||
Before we can run the app, we need to create and initialize the database.
|
|
||||||
|
|
||||||
Similar to Django, first run:
|
|
||||||
|
|
||||||
```
|
|
||||||
ohmyapi makemigrations [ <app> ] # no app means all INSTALLED_APPS
|
|
||||||
```
|
|
||||||
|
|
||||||
This will create a `migrations/` folder in you project root.
|
|
||||||
|
|
||||||
```
|
|
||||||
myproject/
|
|
||||||
- myapp/
|
|
||||||
- __init__.py
|
|
||||||
- models.py
|
|
||||||
- routes.py
|
|
||||||
- migrations/
|
|
||||||
- myapp/
|
|
||||||
- pyproject.toml
|
|
||||||
- settings.py
|
|
||||||
```
|
|
||||||
|
|
||||||
Apply your migrations via:
|
|
||||||
|
|
||||||
```
|
|
||||||
ohmyapi migrate [ <app> ] # no app means all INSTALLED_APPS
|
|
||||||
```
|
|
||||||
|
|
||||||
Run your project:
|
|
||||||
|
|
||||||
```
|
|
||||||
ohmyapi serve
|
|
||||||
```
|
|
||||||
|
|
||||||
## Shell
|
|
||||||
|
|
||||||
Similar to Django, you can attach to an interactive shell with your project already loaded inside.
|
|
||||||
|
|
||||||
```
|
|
||||||
ohmyapi shell
|
|
||||||
```
|
|
||||||
|
|
||||||
## 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 auth tables to be created in the database.
|
|
||||||
|
|
||||||
`settings.py`:
|
|
||||||
|
|
||||||
```
|
|
||||||
INSTALLED_APPS = [
|
|
||||||
'ohmyapi_auth',
|
|
||||||
...
|
|
||||||
]
|
|
||||||
|
|
||||||
JWT_SECRET = "t0ps3cr3t"
|
|
||||||
```
|
|
||||||
|
|
||||||
Create a super-user:
|
|
||||||
|
|
||||||
```
|
|
||||||
ohmyapi createsuperuser
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Go to: [http://localhost:8000/](http://localhost:8000/)
|
||||||
|
|
|
||||||
8
docs/README.md
Normal file
8
docs/README.md
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
# OhMyAPI Docs
|
||||||
|
|
||||||
|
- [Projects](projects.md)
|
||||||
|
- [Apps](apps.md)
|
||||||
|
- [Models](models.md)
|
||||||
|
- [Migrations](migrations.md)
|
||||||
|
- [Routes](routes.md)
|
||||||
|
- [Auth/Permissions](auth.md)
|
||||||
34
docs/apps.md
Normal file
34
docs/apps.md
Normal file
|
|
@ -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` 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 <name>`, 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
|
||||||
|
```
|
||||||
|
|
||||||
45
docs/auth.md
Normal file
45
docs/auth.md
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
# Authentication
|
||||||
|
|
||||||
|
OhMyAPI comes bundled with a builtin authentication app.
|
||||||
|
Simply add `ohmyapi_auth` to your `INSTALLED_APPS` and configure a `JWT_SECRET`.
|
||||||
|
|
||||||
|
## Enable Auth App
|
||||||
|
|
||||||
|
`settings.py`:
|
||||||
|
|
||||||
|
```
|
||||||
|
INSTALLED_APPS = [
|
||||||
|
"ohmyapi_auth",
|
||||||
|
...
|
||||||
|
]
|
||||||
|
|
||||||
|
JWT_SECRET = "t0ps3cr3t"
|
||||||
|
```
|
||||||
|
|
||||||
|
Remember to `makemigrations` and `migrate` to create the necessary database tables.
|
||||||
|
|
||||||
|
```
|
||||||
|
ohmyapi makemigrations
|
||||||
|
ohmyapi migrate
|
||||||
|
```
|
||||||
|
|
||||||
|
## Permissions
|
||||||
|
|
||||||
|
With the `ohmyapi_auth` app comes everything you need to implement API-level permissions.
|
||||||
|
Use FastAPI's `Depends` pattern in combination with either the provided or custom permissions.
|
||||||
|
|
||||||
|
```python
|
||||||
|
from ohmyapi.router import APIRouter, Depends
|
||||||
|
|
||||||
|
from ohmyapi_auth import (
|
||||||
|
models as auth,
|
||||||
|
permissions,
|
||||||
|
)
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/")
|
||||||
|
def get(user: auth.User = Depends(permissions.require_authenticated)):
|
||||||
|
...
|
||||||
|
```
|
||||||
30
docs/index.md
Normal file
30
docs/index.md
Normal file
|
|
@ -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
|
||||||
|
```
|
||||||
|
|
||||||
22
docs/migrations.md
Normal file
22
docs/migrations.md
Normal file
|
|
@ -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 [ <app> ] # 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 [ <app> ] # no app indicates all INSTALLED_APPS
|
||||||
|
```
|
||||||
|
|
||||||
102
docs/models.md
Normal file
102
docs/models.md
Normal file
|
|
@ -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]: <Restaurant>
|
||||||
|
```
|
||||||
|
|
||||||
|
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
|
||||||
|
```
|
||||||
34
docs/projects.md
Normal file
34
docs/projects.md
Normal file
|
|
@ -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)
|
||||||
|
|
||||||
68
docs/routes.md
Normal file
68
docs/routes.md
Normal file
|
|
@ -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()
|
||||||
|
|
||||||
|
```
|
||||||
11
mkdocs.yml
Normal file
11
mkdocs.yml
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
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
|
||||||
|
- Builtin:
|
||||||
|
- Auth / Permissions: auth.md
|
||||||
665
poetry.lock
generated
665
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
||||||
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "aerich"
|
name = "aerich"
|
||||||
|
|
@ -58,22 +58,23 @@ files = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyio"
|
name = "anyio"
|
||||||
version = "4.10.0"
|
version = "4.11.0"
|
||||||
description = "High-level concurrency and networking framework on top of asyncio or Trio"
|
description = "High-level concurrency and networking framework on top of asyncio or Trio"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "anyio-4.10.0-py3-none-any.whl", hash = "sha256:60e474ac86736bbfd6f210f7a61218939c318f43f9972497381f1c5e930ed3d1"},
|
{file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"},
|
||||||
{file = "anyio-4.10.0.tar.gz", hash = "sha256:3f3fae35c96039744587aa5b8371e7e8e603c0702999535961dd336026973ba6"},
|
{file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
idna = ">=2.8"
|
idna = ">=2.8"
|
||||||
sniffio = ">=1.1"
|
sniffio = ">=1.1"
|
||||||
|
typing_extensions = {version = ">=4.5", markers = "python_version < \"3.13\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
trio = ["trio (>=0.26.1)"]
|
trio = ["trio (>=0.31.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "argon2-cffi"
|
name = "argon2-cffi"
|
||||||
|
|
@ -138,7 +139,7 @@ version = "3.0.0"
|
||||||
description = "Annotate AST trees with source code positions"
|
description = "Annotate AST trees with source code positions"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"},
|
{file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"},
|
||||||
{file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"},
|
{file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"},
|
||||||
|
|
@ -163,13 +164,59 @@ files = [
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "black"
|
||||||
|
version = "25.9.0"
|
||||||
|
description = "The uncompromising code formatter."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "black-25.9.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ce41ed2614b706fd55fd0b4a6909d06b5bab344ffbfadc6ef34ae50adba3d4f7"},
|
||||||
|
{file = "black-25.9.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2ab0ce111ef026790e9b13bd216fa7bc48edd934ffc4cbf78808b235793cbc92"},
|
||||||
|
{file = "black-25.9.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f96b6726d690c96c60ba682955199f8c39abc1ae0c3a494a9c62c0184049a713"},
|
||||||
|
{file = "black-25.9.0-cp310-cp310-win_amd64.whl", hash = "sha256:d119957b37cc641596063cd7db2656c5be3752ac17877017b2ffcdb9dfc4d2b1"},
|
||||||
|
{file = "black-25.9.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:456386fe87bad41b806d53c062e2974615825c7a52159cde7ccaeb0695fa28fa"},
|
||||||
|
{file = "black-25.9.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:a16b14a44c1af60a210d8da28e108e13e75a284bf21a9afa6b4571f96ab8bb9d"},
|
||||||
|
{file = "black-25.9.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aaf319612536d502fdd0e88ce52d8f1352b2c0a955cc2798f79eeca9d3af0608"},
|
||||||
|
{file = "black-25.9.0-cp311-cp311-win_amd64.whl", hash = "sha256:c0372a93e16b3954208417bfe448e09b0de5cc721d521866cd9e0acac3c04a1f"},
|
||||||
|
{file = "black-25.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1b9dc70c21ef8b43248f1d86aedd2aaf75ae110b958a7909ad8463c4aa0880b0"},
|
||||||
|
{file = "black-25.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8e46eecf65a095fa62e53245ae2795c90bdecabd53b50c448d0a8bcd0d2e74c4"},
|
||||||
|
{file = "black-25.9.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9101ee58ddc2442199a25cb648d46ba22cd580b00ca4b44234a324e3ec7a0f7e"},
|
||||||
|
{file = "black-25.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:77e7060a00c5ec4b3367c55f39cf9b06e68965a4f2e61cecacd6d0d9b7ec945a"},
|
||||||
|
{file = "black-25.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0172a012f725b792c358d57fe7b6b6e8e67375dd157f64fa7a3097b3ed3e2175"},
|
||||||
|
{file = "black-25.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3bec74ee60f8dfef564b573a96b8930f7b6a538e846123d5ad77ba14a8d7a64f"},
|
||||||
|
{file = "black-25.9.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b756fc75871cb1bcac5499552d771822fd9db5a2bb8db2a7247936ca48f39831"},
|
||||||
|
{file = "black-25.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:846d58e3ce7879ec1ffe816bb9df6d006cd9590515ed5d17db14e17666b2b357"},
|
||||||
|
{file = "black-25.9.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ef69351df3c84485a8beb6f7b8f9721e2009e20ef80a8d619e2d1788b7816d47"},
|
||||||
|
{file = "black-25.9.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e3c1f4cd5e93842774d9ee4ef6cd8d17790e65f44f7cdbaab5f2cf8ccf22a823"},
|
||||||
|
{file = "black-25.9.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:154b06d618233fe468236ba1f0e40823d4eb08b26f5e9261526fde34916b9140"},
|
||||||
|
{file = "black-25.9.0-cp39-cp39-win_amd64.whl", hash = "sha256:e593466de7b998374ea2585a471ba90553283fb9beefcfa430d84a2651ed5933"},
|
||||||
|
{file = "black-25.9.0-py3-none-any.whl", hash = "sha256:474b34c1342cdc157d307b56c4c65bce916480c4a8f6551fdc6bf9b486a7c4ae"},
|
||||||
|
{file = "black-25.9.0.tar.gz", hash = "sha256:0474bca9a0dd1b51791fcc507a4e02078a1c63f6d4e4ae5544b9848c7adfb619"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
click = ">=8.0.0"
|
||||||
|
mypy-extensions = ">=0.4.3"
|
||||||
|
packaging = ">=22.0"
|
||||||
|
pathspec = ">=0.9.0"
|
||||||
|
platformdirs = ">=2"
|
||||||
|
pytokens = ">=0.1.10"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colorama = ["colorama (>=0.4.3)"]
|
||||||
|
d = ["aiohttp (>=3.10)"]
|
||||||
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
|
uvloop = ["uvloop (>=0.15.2)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "certifi"
|
name = "certifi"
|
||||||
version = "2025.8.3"
|
version = "2025.8.3"
|
||||||
description = "Python package for providing Mozilla's CA Bundle."
|
description = "Python package for providing Mozilla's CA Bundle."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"},
|
{file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"},
|
||||||
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
|
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
|
||||||
|
|
@ -367,7 +414,7 @@ version = "8.3.0"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.10"
|
python-versions = ">=3.10"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
|
{file = "click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc"},
|
||||||
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
|
{file = "click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4"},
|
||||||
|
|
@ -382,7 +429,7 @@ version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
|
markers = "platform_system == \"Windows\" or sys_platform == \"win32\""
|
||||||
files = [
|
files = [
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
|
|
@ -411,7 +458,7 @@ version = "5.2.1"
|
||||||
description = "Decorators for Humans"
|
description = "Decorators for Humans"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"},
|
{file = "decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a"},
|
||||||
{file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"},
|
{file = "decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360"},
|
||||||
|
|
@ -441,7 +488,7 @@ version = "2.2.1"
|
||||||
description = "Get the currently executing AST node of a frame, and other information"
|
description = "Get the currently executing AST node of a frame, and other information"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"},
|
{file = "executing-2.2.1-py2.py3-none-any.whl", hash = "sha256:760643d3452b4d777d295bb167ccc74c64a81df23fb5e08eff250c425a4b2017"},
|
||||||
{file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"},
|
{file = "executing-2.2.1.tar.gz", hash = "sha256:3632cc370565f6648cc328b32435bd120a1e4ebb20c77e3fdde9a13cd1e533c4"},
|
||||||
|
|
@ -472,25 +519,90 @@ 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 = ["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)"]
|
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]]
|
[[package]]
|
||||||
name = "h11"
|
name = "h11"
|
||||||
version = "0.16.0"
|
version = "0.16.0"
|
||||||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
description = "A minimal low-level HTTP client."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"},
|
||||||
|
{file = "httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
certifi = "*"
|
||||||
|
h11 = ">=0.16"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
asyncio = ["anyio (>=4.0,<5.0)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (==1.*)"]
|
||||||
|
trio = ["trio (>=0.22.0,<1.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
description = "The next generation HTTP client."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"},
|
||||||
|
{file = "httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
anyio = "*"
|
||||||
|
certifi = "*"
|
||||||
|
httpcore = "==1.*"
|
||||||
|
idna = "*"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
brotli = ["brotli ; platform_python_implementation == \"CPython\"", "brotlicffi ; platform_python_implementation != \"CPython\""]
|
||||||
|
cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"]
|
||||||
|
http2 = ["h2 (>=3,<5)"]
|
||||||
|
socks = ["socksio (==1.*)"]
|
||||||
|
zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "idna"
|
name = "idna"
|
||||||
version = "3.10"
|
version = "3.10"
|
||||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||||
|
|
@ -499,16 +611,28 @@ files = [
|
||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
description = "brain-dead simple config-ini parsing"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.10"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"},
|
||||||
|
{file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipython"
|
name = "ipython"
|
||||||
version = "9.5.0"
|
version = "9.6.0"
|
||||||
description = "IPython: Productive Interactive Computing"
|
description = "IPython: Productive Interactive Computing"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.11"
|
python-versions = ">=3.11"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72"},
|
{file = "ipython-9.6.0-py3-none-any.whl", hash = "sha256:5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196"},
|
||||||
{file = "ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113"},
|
{file = "ipython-9.6.0.tar.gz", hash = "sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -522,14 +646,15 @@ prompt_toolkit = ">=3.0.41,<3.1.0"
|
||||||
pygments = ">=2.4.0"
|
pygments = ">=2.4.0"
|
||||||
stack_data = "*"
|
stack_data = "*"
|
||||||
traitlets = ">=5.13.0"
|
traitlets = ">=5.13.0"
|
||||||
|
typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
all = ["ipython[doc,matplotlib,test,test-extra]"]
|
all = ["ipython[doc,matplotlib,test,test-extra]"]
|
||||||
black = ["black"]
|
black = ["black"]
|
||||||
doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[test]", "matplotlib", "setuptools (>=18.5)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"]
|
doc = ["docrepr", "exceptiongroup", "intersphinx_registry", "ipykernel", "ipython[matplotlib,test]", "setuptools (>=61.2)", "sphinx (>=1.3)", "sphinx-rtd-theme", "sphinx_toml (==0.0.4)", "typing_extensions"]
|
||||||
matplotlib = ["matplotlib"]
|
matplotlib = ["matplotlib (>3.7)"]
|
||||||
test = ["packaging", "pytest", "pytest-asyncio", "testpath"]
|
test = ["packaging", "pytest", "pytest-asyncio", "testpath"]
|
||||||
test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
|
test-extra = ["curio", "ipykernel", "ipython[matplotlib]", "ipython[test]", "jupyter_ai", "nbclient", "nbformat", "numpy (>=1.25)", "pandas (>2.0)", "trio"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ipython-pygments-lexers"
|
name = "ipython-pygments-lexers"
|
||||||
|
|
@ -537,7 +662,7 @@ version = "1.1.1"
|
||||||
description = "Defines a variety of Pygments lexers for highlighting IPython code."
|
description = "Defines a variety of Pygments lexers for highlighting IPython code."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"},
|
{file = "ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c"},
|
||||||
{file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"},
|
{file = "ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81"},
|
||||||
|
|
@ -559,13 +684,29 @@ files = [
|
||||||
{file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"},
|
{file = "iso8601-2.1.0.tar.gz", hash = "sha256:6b1d3829ee8921c4301998c909f7829fa9ed3cbdac0d3b16af2d743aed1ba8df"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "isort"
|
||||||
|
version = "6.1.0"
|
||||||
|
description = "A Python utility / library to sort Python imports."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9.0"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"},
|
||||||
|
{file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
colors = ["colorama"]
|
||||||
|
plugins = ["setuptools"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "jedi"
|
name = "jedi"
|
||||||
version = "0.19.2"
|
version = "0.19.2"
|
||||||
description = "An autocompletion tool for Python that can be used for text editors."
|
description = "An autocompletion tool for Python that can be used for text editors."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
|
{file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"},
|
||||||
{file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
|
{file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"},
|
||||||
|
|
@ -585,7 +726,7 @@ version = "3.1.6"
|
||||||
description = "A very fast and expressive template engine."
|
description = "A very fast and expressive template engine."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
|
{file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"},
|
||||||
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
|
{file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"},
|
||||||
|
|
@ -597,6 +738,22 @@ MarkupSafe = ">=2.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
i18n = ["Babel (>=2.7)"]
|
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]]
|
[[package]]
|
||||||
name = "markdown-it-py"
|
name = "markdown-it-py"
|
||||||
version = "4.0.0"
|
version = "4.0.0"
|
||||||
|
|
@ -623,73 +780,101 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "markupsafe"
|
name = "markupsafe"
|
||||||
version = "3.0.2"
|
version = "3.0.3"
|
||||||
description = "Safely add untrusted strings to HTML/XML markup."
|
description = "Safely add untrusted strings to HTML/XML markup."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
|
{file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
|
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
|
{file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
|
{file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"},
|
{file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
|
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
|
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
|
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
|
{file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
|
||||||
{file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
|
{file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
|
{file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
|
{file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
|
{file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
|
{file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"},
|
{file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
|
{file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
|
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
|
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
|
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
|
||||||
{file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
|
{file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
|
{file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
|
{file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
|
{file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
|
{file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"},
|
{file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
|
{file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
|
{file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
|
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
|
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
|
||||||
{file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
|
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
|
{file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
|
{file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
|
{file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
|
{file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"},
|
{file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
|
{file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
|
{file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
|
{file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
|
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
|
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
|
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
|
{file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
|
{file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
|
{file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
|
||||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"},
|
{file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
|
{file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
|
{file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
|
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
|
{file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
|
||||||
{file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
|
{file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
|
||||||
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"},
|
||||||
|
{file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"},
|
||||||
|
{file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"},
|
||||||
|
{file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -698,7 +883,7 @@ version = "0.1.7"
|
||||||
description = "Inline Matplotlib backend for Jupyter"
|
description = "Inline Matplotlib backend for Jupyter"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
|
{file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"},
|
||||||
{file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
|
{file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"},
|
||||||
|
|
@ -719,6 +904,78 @@ files = [
|
||||||
{file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"},
|
{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"
|
||||||
|
description = "Type system extensions for programs checked with the mypy type checker."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"},
|
||||||
|
{file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "naked"
|
name = "naked"
|
||||||
version = "0.1.32"
|
version = "0.1.32"
|
||||||
|
|
@ -735,13 +992,25 @@ files = [
|
||||||
pyyaml = "*"
|
pyyaml = "*"
|
||||||
requests = "*"
|
requests = "*"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "25.0"
|
||||||
|
description = "Core utilities for Python packages"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"},
|
||||||
|
{file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parso"
|
name = "parso"
|
||||||
version = "0.8.5"
|
version = "0.8.5"
|
||||||
description = "A Python Parser"
|
description = "A Python Parser"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"},
|
{file = "parso-0.8.5-py2.py3-none-any.whl", hash = "sha256:646204b5ee239c396d040b90f9e272e9a8017c630092bf59980beb62fd033887"},
|
||||||
{file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"},
|
{file = "parso-0.8.5.tar.gz", hash = "sha256:034d7354a9a018bdce352f48b2a8a450f05e9d6ee85db84764e9b6bd96dafe5a"},
|
||||||
|
|
@ -769,13 +1038,25 @@ bcrypt = ["bcrypt (>=3.1.0)"]
|
||||||
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
build-docs = ["cloud-sptheme (>=1.10.1)", "sphinx (>=1.6)", "sphinxcontrib-fulltoc (>=1.2.0)"]
|
||||||
totp = ["cryptography"]
|
totp = ["cryptography"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pathspec"
|
||||||
|
version = "0.12.1"
|
||||||
|
description = "Utility library for gitignore style pattern matching of file paths."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"},
|
||||||
|
{file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pexpect"
|
name = "pexpect"
|
||||||
version = "4.9.0"
|
version = "4.9.0"
|
||||||
description = "Pexpect allows easy control of interactive console applications."
|
description = "Pexpect allows easy control of interactive console applications."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
|
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
|
||||||
files = [
|
files = [
|
||||||
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
|
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
|
||||||
|
|
@ -785,13 +1066,46 @@ files = [
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
ptyprocess = ">=0.5"
|
ptyprocess = ">=0.5"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "platformdirs"
|
||||||
|
version = "4.4.0"
|
||||||
|
description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85"},
|
||||||
|
{file = "platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"]
|
||||||
|
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"]
|
||||||
|
type = ["mypy (>=1.14.1)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
description = "plugin and hook calling mechanisms for python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"},
|
||||||
|
{file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["pre-commit", "tox"]
|
||||||
|
testing = ["coverage", "pytest", "pytest-benchmark"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "prompt-toolkit"
|
name = "prompt-toolkit"
|
||||||
version = "3.0.52"
|
version = "3.0.52"
|
||||||
description = "Library for building powerful interactive command lines in Python"
|
description = "Library for building powerful interactive command lines in Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"},
|
{file = "prompt_toolkit-3.0.52-py3-none-any.whl", hash = "sha256:9aac639a3bbd33284347de5ad8d68ecc044b91a762dc39b7c21095fcd6a19955"},
|
||||||
{file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"},
|
{file = "prompt_toolkit-3.0.52.tar.gz", hash = "sha256:28cde192929c8e7321de85de1ddbe736f1375148b02f2e17edd840042b1be855"},
|
||||||
|
|
@ -806,7 +1120,7 @@ version = "0.7.0"
|
||||||
description = "Run a subprocess in a pseudo terminal"
|
description = "Run a subprocess in a pseudo terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
|
markers = "sys_platform != \"win32\" and sys_platform != \"emscripten\""
|
||||||
files = [
|
files = [
|
||||||
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
||||||
|
|
@ -819,7 +1133,7 @@ version = "0.2.3"
|
||||||
description = "Safely evaluate AST nodes without side effects"
|
description = "Safely evaluate AST nodes without side effects"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
|
{file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"},
|
||||||
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
|
{file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"},
|
||||||
|
|
@ -981,7 +1295,7 @@ version = "2.19.2"
|
||||||
description = "Pygments is a syntax highlighting package written in Python."
|
description = "Pygments is a syntax highlighting package written in Python."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
|
{file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"},
|
||||||
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
|
{file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"},
|
||||||
|
|
@ -1021,6 +1335,43 @@ files = [
|
||||||
{file = "pypika_tortoise-0.6.2.tar.gz", hash = "sha256:f95ab59d9b6454db2e8daa0934728458350a1f3d56e81d9d1debc8eebeff26b3"},
|
{file = "pypika_tortoise-0.6.2.tar.gz", hash = "sha256:f95ab59d9b6454db2e8daa0934728458350a1f3d56e81d9d1debc8eebeff26b3"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytest"
|
||||||
|
version = "8.4.2"
|
||||||
|
description = "pytest: simple powerful testing with Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pytest-8.4.2-py3-none-any.whl", hash = "sha256:872f880de3fc3a5bdc88a11b39c9710c3497a547cfa9320bc3c5e62fbf272e79"},
|
||||||
|
{file = "pytest-8.4.2.tar.gz", hash = "sha256:86c0d0b93306b961d58d62a4db4879f27fe25513d4b969df351abdddb3c30e01"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""}
|
||||||
|
iniconfig = ">=1"
|
||||||
|
packaging = ">=20"
|
||||||
|
pluggy = ">=1.5,<2"
|
||||||
|
pygments = ">=2.7.2"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"]
|
||||||
|
|
||||||
|
[[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]]
|
[[package]]
|
||||||
name = "python-multipart"
|
name = "python-multipart"
|
||||||
version = "0.0.20"
|
version = "0.0.20"
|
||||||
|
|
@ -1033,6 +1384,21 @@ files = [
|
||||||
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
|
{file = "python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pytokens"
|
||||||
|
version = "0.1.10"
|
||||||
|
description = "A Fast, spec compliant Python 3.12+ tokenizer that runs on older Pythons."
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
groups = ["dev"]
|
||||||
|
files = [
|
||||||
|
{file = "pytokens-0.1.10-py3-none-any.whl", hash = "sha256:db7b72284e480e69fb085d9f251f66b3d2df8b7166059261258ff35f50fb711b"},
|
||||||
|
{file = "pytokens-0.1.10.tar.gz", hash = "sha256:c9a4bfa0be1d26aebce03e6884ba454e842f186a59ea43a6d3b25af58223c044"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
dev = ["black", "build", "mypy", "pytest", "pytest-cov", "setuptools", "tox", "twine", "wheel"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "pytz"
|
name = "pytz"
|
||||||
version = "2025.2"
|
version = "2025.2"
|
||||||
|
|
@ -1051,7 +1417,7 @@ version = "6.0.3"
|
||||||
description = "YAML parser and emitter for Python"
|
description = "YAML parser and emitter for Python"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
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_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"},
|
||||||
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
{file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"},
|
||||||
|
|
@ -1121,6 +1487,21 @@ files = [
|
||||||
{file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"},
|
{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]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.5"
|
version = "2.32.5"
|
||||||
|
|
@ -1186,13 +1567,25 @@ files = [
|
||||||
{file = "shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de"},
|
{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]]
|
[[package]]
|
||||||
name = "sniffio"
|
name = "sniffio"
|
||||||
version = "1.3.1"
|
version = "1.3.1"
|
||||||
description = "Sniff out which async library your code is running under"
|
description = "Sniff out which async library your code is running under"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||||
|
|
@ -1204,7 +1597,7 @@ version = "0.6.3"
|
||||||
description = "Extract data from python stack frames and tracebacks for informative displays"
|
description = "Extract data from python stack frames and tracebacks for informative displays"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
python-versions = "*"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
|
{file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"},
|
||||||
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
|
{file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"},
|
||||||
|
|
@ -1232,6 +1625,7 @@ files = [
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
anyio = ">=3.6.2,<5"
|
anyio = ">=3.6.2,<5"
|
||||||
|
typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""}
|
||||||
|
|
||||||
[package.extras]
|
[package.extras]
|
||||||
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
||||||
|
|
@ -1268,7 +1662,7 @@ version = "5.14.3"
|
||||||
description = "Traitlets Python configuration system"
|
description = "Traitlets Python configuration system"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.8"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
|
{file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"},
|
||||||
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
|
{file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"},
|
||||||
|
|
@ -1280,14 +1674,14 @@ test = ["argcomplete (>=3.0.3)", "mypy (>=1.7.0)", "pre-commit", "pytest (>=7.0,
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typer"
|
name = "typer"
|
||||||
version = "0.19.1"
|
version = "0.19.2"
|
||||||
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
description = "Typer, build great CLIs. Easy to code. Based on Python type hints."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.8"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typer-0.19.1-py3-none-any.whl", hash = "sha256:914b2b39a1da4bafca5f30637ca26fa622a5bf9f515e5fdc772439f306d5682a"},
|
{file = "typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9"},
|
||||||
{file = "typer-0.19.1.tar.gz", hash = "sha256:cb881433a4b15dacc875bb0583d1a61e78497806741f9aba792abcab390c03e6"},
|
{file = "typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -1302,22 +1696,23 @@ version = "4.15.0"
|
||||||
description = "Backported and Experimental Type Hints for Python 3.9+"
|
description = "Backported and Experimental Type Hints for Python 3.9+"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||||
]
|
]
|
||||||
|
markers = {dev = "python_version < \"3.13\""}
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "typing-inspection"
|
name = "typing-inspection"
|
||||||
version = "0.4.1"
|
version = "0.4.2"
|
||||||
description = "Runtime typing introspection tools"
|
description = "Runtime typing introspection tools"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
|
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
|
||||||
{file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
|
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -1343,14 +1738,14 @@ zstd = ["zstandard (>=0.18.0)"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "uvicorn"
|
name = "uvicorn"
|
||||||
version = "0.36.0"
|
version = "0.36.1"
|
||||||
description = "The lightning-fast ASGI server."
|
description = "The lightning-fast ASGI server."
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.9"
|
python-versions = ">=3.9"
|
||||||
groups = ["main"]
|
groups = ["main"]
|
||||||
files = [
|
files = [
|
||||||
{file = "uvicorn-0.36.0-py3-none-any.whl", hash = "sha256:6bb4ba67f16024883af8adf13aba3a9919e415358604ce46780d3f9bdc36d731"},
|
{file = "uvicorn-0.36.1-py3-none-any.whl", hash = "sha256:059086ecb470a021553f17bf860fce2095611d92fb8b669c44325b3435a0a654"},
|
||||||
{file = "uvicorn-0.36.0.tar.gz", hash = "sha256:527dc68d77819919d90a6b267be55f0e76704dca829d34aea9480be831a9b9d9"},
|
{file = "uvicorn-0.36.1.tar.gz", hash = "sha256:048e68f2a0fe291cd848ed076f18c026e1b0bc69991495f087634ac9a41e8706"},
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.dependencies]
|
[package.dependencies]
|
||||||
|
|
@ -1360,19 +1755,65 @@ h11 = ">=0.8"
|
||||||
[package.extras]
|
[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)"]
|
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]]
|
[[package]]
|
||||||
name = "wcwidth"
|
name = "wcwidth"
|
||||||
version = "0.2.14"
|
version = "0.2.14"
|
||||||
description = "Measures the displayed width of unicode strings in a terminal"
|
description = "Measures the displayed width of unicode strings in a terminal"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.6"
|
python-versions = ">=3.6"
|
||||||
groups = ["main"]
|
groups = ["main", "dev"]
|
||||||
files = [
|
files = [
|
||||||
{file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"},
|
{file = "wcwidth-0.2.14-py2.py3-none-any.whl", hash = "sha256:a7bb560c8aee30f9957e5f9895805edd20602f2d7f720186dfd906e82b4982e1"},
|
||||||
{file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"},
|
{file = "wcwidth-0.2.14.tar.gz", hash = "sha256:4d478375d31bc5395a3c55c40ccdf3354688364cd61c4f6adacaa9215d0b3605"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[extras]
|
||||||
|
auth = ["argon2-cffi", "crypto", "passlib", "pyjwt", "python-multipart"]
|
||||||
|
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.1"
|
lock-version = "2.1"
|
||||||
python-versions = ">=3.13"
|
python-versions = ">=3.11"
|
||||||
content-hash = "b1f2f4159b02bf80e7bf6f995933ce4417ec6e1cff299da5157dda50e7da0bb6"
|
content-hash = "cc1604995d3b73ee302e63731dd300ea17c4d95d0cfc6c386626dd9a9f60e8a7"
|
||||||
|
|
|
||||||
|
|
@ -1,36 +1,79 @@
|
||||||
[project]
|
[project]
|
||||||
name = "ohmyapi"
|
name = "ohmyapi"
|
||||||
version = "0.1.3"
|
version = "0.6.2"
|
||||||
description = "A Django-like but async web-framework based on FastAPI and TortoiseORM."
|
description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations"
|
||||||
license = "MIT"
|
license = "MIT"
|
||||||
keywords = ["fastapi", "tortoise", "orm", "async", "web-framework"]
|
keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"]
|
||||||
authors = [
|
authors = [
|
||||||
{name = "Brian Wiborg", email = "me@brianwib.org"}
|
{name = "Brian Wiborg", email = "me@brianwib.org"}
|
||||||
]
|
]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
requires-python = ">=3.13"
|
requires-python = ">=3.11"
|
||||||
|
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"typer (>=0.19.1,<0.20.0)",
|
"typer >=0.19.1,<0.20.0",
|
||||||
"jinja2 (>=3.1.6,<4.0.0)",
|
"jinja2 >=3.1.6,<4.0.0",
|
||||||
"fastapi (>=0.117.1,<0.118.0)",
|
"fastapi >=0.117.1,<0.118.0",
|
||||||
"tortoise-orm (>=0.25.1,<0.26.0)",
|
"tortoise-orm >=0.25.1,<0.26.0",
|
||||||
"aerich (>=0.9.1,<0.10.0)",
|
"aerich >=0.9.1,<0.10.0",
|
||||||
"uvicorn (>=0.36.0,<0.37.0)",
|
"uvicorn >=0.36.0,<0.37.0",
|
||||||
"ipython (>=9.5.0,<10.0.0)",
|
"ipython >=9.5.0,<10.0.0",
|
||||||
"passlib (>=1.7.4,<2.0.0)",
|
"passlib >=1.7.4,<2.0.0",
|
||||||
"pyjwt (>=2.10.1,<3.0.0)",
|
"pyjwt >=2.10.1,<3.0.0",
|
||||||
"python-multipart (>=0.0.20,<0.0.21)",
|
"python-multipart >=0.0.20,<0.0.21",
|
||||||
"crypto (>=1.4.1,<2.0.0)",
|
"crypto >=1.4.1,<2.0.0",
|
||||||
"argon2-cffi (>=25.1.0,<26.0.0)",
|
"argon2-cffi >=25.1.0,<26.0.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
ipython = ">=9.5.0,<10.0.0"
|
||||||
|
black = "^25.9.0"
|
||||||
|
isort = "^6.0.1"
|
||||||
|
mkdocs = "^1.6.1"
|
||||||
|
pytest = "^8.4.2"
|
||||||
|
httpx = "^0.28.1"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
auth = ["passlib", "pyjwt", "crypto", "argon2-cffi", "python-multipart"]
|
||||||
|
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
packages = [ { include = "ohmyapi", from = "src" } ]
|
packages = [ { include = "ohmyapi", from = "src" } ]
|
||||||
|
|
||||||
[tool.poetry.scripts]
|
[project.scripts]
|
||||||
ohmyapi = "ohmyapi.cli:main"
|
ohmyapi = "ohmyapi.cli:app"
|
||||||
|
|
||||||
|
[project.urls]
|
||||||
|
repository = "https://code.c-base.org/baccenfutter/ohmyapi"
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core>=2.0.0,<3.0.0"]
|
requires = [
|
||||||
build-backend = "poetry.core.masonry.api"
|
"poetry-core>=1.8.0",
|
||||||
|
"poetry-dynamic-versioning>=1.8.0"
|
||||||
|
]
|
||||||
|
build-backend = "poetry_dynamic_versioning.backend"
|
||||||
|
|
||||||
|
[tool.poetry-dynamic-versioning]
|
||||||
|
enable = true
|
||||||
|
source = "file"
|
||||||
|
path = "src/ohmyapi/__init__.py"
|
||||||
|
pattern = "__VERSION__\\s*=\\s*['\"](?P<version>[^'\"]+)['\"]"
|
||||||
|
|
||||||
|
[tool.black]
|
||||||
|
line-length = 88
|
||||||
|
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
|
||||||
|
include = '\.pyi?$'
|
||||||
|
exclude = '''
|
||||||
|
/(
|
||||||
|
\.git
|
||||||
|
| \.venv
|
||||||
|
| build
|
||||||
|
| dist
|
||||||
|
)/
|
||||||
|
'''
|
||||||
|
|
||||||
|
[tool.isort]
|
||||||
|
profile = "black" # makes imports compatible with black
|
||||||
|
line_length = 88
|
||||||
|
multi_line_output = 3
|
||||||
|
include_trailing_comma = true
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,2 +1 @@
|
||||||
from . import db
|
__VERSION__ = "0.6.2"
|
||||||
|
|
||||||
|
|
|
||||||
3
src/ohmyapi/__main__.py
Normal file
3
src/ohmyapi/__main__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
from .cli import app
|
||||||
|
|
||||||
|
app()
|
||||||
|
|
@ -1,4 +1 @@
|
||||||
from . import models
|
from . import models, permissions, routes
|
||||||
from . import routes
|
|
||||||
from . import permissions
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,34 +1,52 @@
|
||||||
from typing import Optional, List
|
from ohmyapi.db import Model, field, Q
|
||||||
from ohmyapi.db import Model, field
|
from ohmyapi.router import HTTPException
|
||||||
|
|
||||||
|
from .utils import hmac_hash
|
||||||
|
|
||||||
|
from datetime import datetime
|
||||||
from passlib.context import CryptContext
|
from passlib.context import CryptContext
|
||||||
from tortoise.contrib.pydantic import pydantic_queryset_creator
|
from typing import Optional
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||||
|
|
||||||
|
|
||||||
class Group(Model):
|
|
||||||
id = field.IntField(pk=True)
|
|
||||||
name = field.CharField(max_length=42, index=True)
|
|
||||||
|
|
||||||
|
|
||||||
class User(Model):
|
class User(Model):
|
||||||
id = field.IntField(pk=True)
|
id: UUID = field.data.UUIDField(pk=True)
|
||||||
email = field.CharField(unique=True, index=True)
|
username: str = field.CharField(max_length=150, unique=True)
|
||||||
username = field.CharField(max_length=150, unique=True)
|
email_hash: str = field.CharField(max_length=255, unique=True, index=True)
|
||||||
password_hash = field.CharField(max_length=128)
|
password_hash: str = field.CharField(max_length=128)
|
||||||
is_admin = field.BooleanField(default=False)
|
is_admin: bool = field.BooleanField(default=False)
|
||||||
is_staff = field.BooleanField(default=False)
|
is_staff: bool = field.BooleanField(default=False)
|
||||||
groups: Optional[List[Group]] = field.ManyToManyField("ohmyapi_auth.Group", related_name="users")
|
created_at: datetime = field.DatetimeField(auto_now_add=True)
|
||||||
|
updated_at: datetime = field.DatetimeField(auto_now=True)
|
||||||
|
|
||||||
class Schema:
|
class Schema:
|
||||||
exclude = 'password_hash',
|
include = {
|
||||||
|
"id",
|
||||||
|
"username",
|
||||||
|
"is_admin",
|
||||||
|
"is_staff"
|
||||||
|
"created_at",
|
||||||
|
"updated_at",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
fields = {
|
||||||
|
'username': self.username,
|
||||||
|
'is_admin': 'y' if self.is_admin else 'n',
|
||||||
|
'is_staff': 'y' if self.is_staff else 'n',
|
||||||
|
}
|
||||||
|
return ' '.join([f"{k}:{v}" for k, v in fields.items()])
|
||||||
|
|
||||||
def set_password(self, raw_password: str) -> None:
|
def set_password(self, raw_password: str) -> None:
|
||||||
"""Hash and store the password."""
|
"""Hash and store the password."""
|
||||||
self.password_hash = pwd_context.hash(raw_password)
|
self.password_hash = pwd_context.hash(raw_password)
|
||||||
|
|
||||||
|
def set_email(self, new_email: str) -> None:
|
||||||
|
"""Hash and set the e-mail address."""
|
||||||
|
self.email_hash = hmac_hash(new_email)
|
||||||
|
|
||||||
def verify_password(self, raw_password: str) -> bool:
|
def verify_password(self, raw_password: str) -> bool:
|
||||||
"""Verify a plaintext password against the stored hash."""
|
"""Verify a plaintext password against the stored hash."""
|
||||||
return pwd_context.verify(raw_password, self.password_hash)
|
return pwd_context.verify(raw_password, self.password_hash)
|
||||||
|
|
@ -40,4 +58,3 @@ class User(Model):
|
||||||
if user and user.verify_password(password):
|
if user and user.verify_password(password):
|
||||||
return user
|
return user
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
from .routes import (
|
from .routes import (
|
||||||
get_current_user,
|
get_current_user,
|
||||||
require_authenticated,
|
get_token,
|
||||||
|
maybe_authenticated,
|
||||||
require_admin,
|
require_admin,
|
||||||
|
require_authenticated,
|
||||||
|
require_group,
|
||||||
require_staff,
|
require_staff,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,100 @@
|
||||||
import time
|
import time
|
||||||
from typing import Dict
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
import jwt
|
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Request, status
|
||||||
from fastapi import APIRouter, Body, Depends, Header, HTTPException, status
|
from fastapi.security import OAuth2, OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
||||||
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
from fastapi.security.utils import get_authorization_scheme_param
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
from tortoise.exceptions import DoesNotExist
|
||||||
|
|
||||||
from ohmyapi.builtin.auth.models import User
|
from ohmyapi.builtin.auth.models import User
|
||||||
|
|
||||||
|
import jwt
|
||||||
import settings
|
import settings
|
||||||
|
|
||||||
# Router
|
# Router
|
||||||
router = APIRouter(prefix="/auth", tags=["auth"])
|
router = APIRouter(prefix="/auth", tags=["Auth"])
|
||||||
|
|
||||||
# Secrets & config (should come from settings/env in real projects)
|
# Secrets & config (should come from settings/env in real projects)
|
||||||
JWT_SECRET = getattr(settings, "JWT_SECRET", "changeme")
|
JWT_SECRET = getattr(settings, "JWT_SECRET", "changeme")
|
||||||
JWT_ALGORITHM = getattr(settings, "JWT_ALGORITHM", "HS256")
|
JWT_ALGORITHM = getattr(settings, "JWT_ALGORITHM", "HS256")
|
||||||
ACCESS_TOKEN_EXPIRE_SECONDS = getattr(settings, "JWT_ACCESS_TOKEN_EXPIRE_SECONDS", 15 * 60)
|
ACCESS_TOKEN_EXPIRE_SECONDS = getattr(
|
||||||
REFRESH_TOKEN_EXPIRE_SECONDS = getattr(settings, "JWT_REFRESH_TOKEN_EXPIRE_SECONDS", 7 * 24 * 60 * 60)
|
settings, "JWT_ACCESS_TOKEN_EXPIRE_SECONDS", 15 * 60
|
||||||
|
)
|
||||||
|
REFRESH_TOKEN_EXPIRE_SECONDS = getattr(
|
||||||
|
settings, "JWT_REFRESH_TOKEN_EXPIRE_SECONDS", 7 * 24 * 60 * 60
|
||||||
|
)
|
||||||
|
|
||||||
|
class OptionalOAuth2PasswordBearer(OAuth2):
|
||||||
|
def __init__(self, tokenUrl: str):
|
||||||
|
super().__init__(flows={"password": {"tokenUrl": tokenUrl}}, scheme_name="OAuth2PasswordBearer")
|
||||||
|
|
||||||
|
async def __call__(self, request: Request) -> Optional[str]:
|
||||||
|
authorization: str = request.headers.get("Authorization")
|
||||||
|
scheme, param = get_authorization_scheme_param(authorization)
|
||||||
|
if not authorization or scheme.lower() != "bearer":
|
||||||
|
# No token provided — just return None
|
||||||
|
return None
|
||||||
|
return param
|
||||||
|
|
||||||
|
|
||||||
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
|
||||||
|
oauth2_optional_scheme = OptionalOAuth2PasswordBearer(tokenUrl="/auth/login")
|
||||||
|
|
||||||
|
|
||||||
def create_token(data: dict, expires_in: int) -> str:
|
class ClaimsUser(BaseModel):
|
||||||
to_encode = data.copy()
|
username: str
|
||||||
to_encode.update({"exp": int(time.time()) + expires_in})
|
is_admin: bool
|
||||||
|
is_staff: bool
|
||||||
|
|
||||||
|
|
||||||
|
class Claims(BaseModel):
|
||||||
|
type: str
|
||||||
|
sub: str
|
||||||
|
user: ClaimsUser
|
||||||
|
exp: str
|
||||||
|
|
||||||
|
|
||||||
|
class AccessToken(BaseModel):
|
||||||
|
token_type: str
|
||||||
|
access_token: str
|
||||||
|
|
||||||
|
|
||||||
|
class RefreshToken(AccessToken):
|
||||||
|
refresh_token: str
|
||||||
|
|
||||||
|
|
||||||
|
class LoginRequest(BaseModel):
|
||||||
|
username: str
|
||||||
|
password: str
|
||||||
|
|
||||||
|
|
||||||
|
class TokenType(str, Enum):
|
||||||
|
"""
|
||||||
|
Helper for indicating the token type when generating claims.
|
||||||
|
"""
|
||||||
|
|
||||||
|
access = "access"
|
||||||
|
refresh = "refresh"
|
||||||
|
|
||||||
|
|
||||||
|
def claims(token_type: TokenType, user: User = []) -> Claims:
|
||||||
|
return Claims(
|
||||||
|
type=token_type,
|
||||||
|
sub=str(user.id),
|
||||||
|
user=ClaimsUser(
|
||||||
|
username=user.username,
|
||||||
|
is_admin=user.is_admin,
|
||||||
|
is_staff=user.is_staff,
|
||||||
|
),
|
||||||
|
exp="",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def create_token(claims: Claims, expires_in: int) -> str:
|
||||||
|
to_encode = claims.model_dump()
|
||||||
|
to_encode["exp"] = int(time.time()) + expires_in
|
||||||
token = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
token = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
|
||||||
if isinstance(token, bytes):
|
if isinstance(token, bytes):
|
||||||
token = token.decode("utf-8")
|
token = token.decode("utf-8")
|
||||||
|
|
@ -35,24 +105,44 @@ def decode_token(token: str) -> Dict:
|
||||||
try:
|
try:
|
||||||
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
|
||||||
except jwt.ExpiredSignatureError:
|
except jwt.ExpiredSignatureError:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
|
||||||
|
)
|
||||||
except jwt.InvalidTokenError:
|
except jwt.InvalidTokenError:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def get_token(token: str = Depends(oauth2_scheme)) -> Dict:
|
||||||
|
"""Dependency: token introspection"""
|
||||||
|
payload = decode_token(token)
|
||||||
|
return payload
|
||||||
|
|
||||||
|
|
||||||
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
|
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
|
||||||
"""Dependency: extract user from access token."""
|
"""Dependency: extract user from access token."""
|
||||||
payload = decode_token(token)
|
payload = decode_token(token)
|
||||||
username = payload.get("sub")
|
user_id = payload.get("sub")
|
||||||
if username is None:
|
if user_id is None:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload"
|
||||||
|
)
|
||||||
|
|
||||||
user = await User.filter(username=username).first()
|
user = await User.filter(id=user_id).first()
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"
|
||||||
|
)
|
||||||
return user
|
return user
|
||||||
|
|
||||||
|
|
||||||
|
async def maybe_authenticated(token: Optional[str] = Depends(oauth2_optional_scheme)) -> Optional[User]:
|
||||||
|
if token is None:
|
||||||
|
return None
|
||||||
|
return await get_current_user(token)
|
||||||
|
|
||||||
|
|
||||||
async def require_authenticated(current_user: User = Depends(get_current_user)) -> User:
|
async def require_authenticated(current_user: User = Depends(get_current_user)) -> User:
|
||||||
"""Ensure the current user is an admin."""
|
"""Ensure the current user is an admin."""
|
||||||
if not current_user:
|
if not current_user:
|
||||||
|
|
@ -69,68 +159,82 @@ async def require_admin(current_user: User = Depends(get_current_user)) -> User:
|
||||||
|
|
||||||
async def require_staff(current_user: User = Depends(get_current_user)) -> User:
|
async def require_staff(current_user: User = Depends(get_current_user)) -> User:
|
||||||
"""Ensure the current user is a staff member."""
|
"""Ensure the current user is a staff member."""
|
||||||
if not current_user.is_staff:
|
if not current_user.is_admin and not current_user.is_staff:
|
||||||
raise HTTPException(403, "Staff privileges required")
|
raise HTTPException(403, "Staff privileges required")
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
async def require_group(
|
async def require_group(
|
||||||
group_name: str,
|
group_name: str, current_user: User = Depends(get_current_user)
|
||||||
current_user: User = Depends(get_current_user)
|
|
||||||
) -> User:
|
) -> User:
|
||||||
"""Ensure the current user belongs to the given group."""
|
"""Ensure the current user belongs to the given group."""
|
||||||
user_groups = await current_user.groups.all()
|
user_groups = await current_user.groups.all()
|
||||||
if not any(g.name == group_name for g in user_groups):
|
if not any(g.name == group_name for g in user_groups):
|
||||||
raise HTTPException(
|
raise HTTPException(
|
||||||
status_code=403,
|
status_code=403, detail=f"User must belong to group '{group_name}'"
|
||||||
detail=f"User must belong to group '{group_name}'"
|
|
||||||
)
|
)
|
||||||
return current_user
|
return current_user
|
||||||
|
|
||||||
|
|
||||||
class LoginRequest(BaseModel):
|
@router.post("/login", response_model=RefreshToken)
|
||||||
username: str
|
|
||||||
password: str
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/login")
|
|
||||||
async def login(form_data: LoginRequest = Body(...)):
|
async def login(form_data: LoginRequest = Body(...)):
|
||||||
"""Login with username & password, returns access and refresh tokens."""
|
"""Login with username & password, returns access and refresh tokens."""
|
||||||
user = await User.authenticate(form_data.username, form_data.password)
|
user = await User.authenticate(form_data.username, form_data.password)
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
|
||||||
|
)
|
||||||
|
|
||||||
access_token = create_token({"sub": user.username, "type": "access"}, ACCESS_TOKEN_EXPIRE_SECONDS)
|
access_token = create_token(
|
||||||
refresh_token = create_token({"sub": user.username, "type": "refresh"}, REFRESH_TOKEN_EXPIRE_SECONDS)
|
claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS
|
||||||
|
)
|
||||||
|
refresh_token = create_token(
|
||||||
|
claims(TokenType.refresh, user), REFRESH_TOKEN_EXPIRE_SECONDS
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return RefreshToken(
|
||||||
"access_token": access_token,
|
token_type="bearer",
|
||||||
"refresh_token": refresh_token,
|
access_token=access_token,
|
||||||
"token_type": "bearer"
|
refresh_token=refresh_token,
|
||||||
}
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.post("/refresh")
|
class TokenRefresh(BaseModel):
|
||||||
async def refresh_token(refresh_token: str):
|
refresh_token: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/refresh", response_model=AccessToken)
|
||||||
|
async def refresh_token(refresh_token: TokenRefresh = Body(...)):
|
||||||
"""Exchange refresh token for new access token."""
|
"""Exchange refresh token for new access token."""
|
||||||
payload = decode_token(refresh_token)
|
payload = decode_token(refresh_token.refresh_token)
|
||||||
if payload.get("type") != "refresh":
|
if payload.get("type") != "refresh":
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
|
||||||
|
)
|
||||||
|
|
||||||
|
user_id = payload.get("sub")
|
||||||
|
try:
|
||||||
|
user = await User.get(id=user_id)
|
||||||
|
except DoesNotExist:
|
||||||
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
|
||||||
|
|
||||||
username = payload.get("sub")
|
|
||||||
user = await User.filter(username=username).first()
|
|
||||||
if not user:
|
if not user:
|
||||||
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
raise HTTPException(
|
||||||
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"
|
||||||
|
)
|
||||||
|
|
||||||
new_access = create_token({"sub": user.username, "type": "access"}, ACCESS_TOKEN_EXPIRE_SECONDS)
|
new_access = create_token(
|
||||||
return {"access_token": new_access, "token_type": "bearer"}
|
claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS
|
||||||
|
)
|
||||||
|
return AccessToken(token_type="bearer", access_token=new_access)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/me")
|
@router.get("/introspect", response_model=Dict[str, Any])
|
||||||
async def me(current_user: User = Depends(get_current_user)):
|
async def introspect(token: Dict = Depends(get_token)):
|
||||||
|
return token
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/me", response_model=User.Schema())
|
||||||
|
async def me(user: User = Depends(get_current_user)):
|
||||||
"""Return the currently authenticated user."""
|
"""Return the currently authenticated user."""
|
||||||
return {
|
return await User.Schema().from_tortoise_orm(user)
|
||||||
"username": current_user.username,
|
|
||||||
"is_admin": current_user.is_admin,
|
|
||||||
"is_staff": current_user.is_staff,
|
|
||||||
}
|
|
||||||
|
|
|
||||||
17
src/ohmyapi/builtin/auth/utils.py
Normal file
17
src/ohmyapi/builtin/auth/utils.py
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
SECRET_KEY = getattr(settings, "SECRET_KEY", "OhMyAPI Secret Key")
|
||||||
|
|
||||||
|
|
||||||
|
def hmac_hash(data: str) -> str:
|
||||||
|
digest = hmac.new(
|
||||||
|
SECRET_KEY.encode("UTF-8"),
|
||||||
|
data.encode("utf-8"),
|
||||||
|
hashlib.sha256,
|
||||||
|
).digest()
|
||||||
|
return base64.urlsafe_b64encode(digest).decode("utf-8")
|
||||||
|
|
||||||
1
src/ohmyapi/builtin/demo/__init__.py
Normal file
1
src/ohmyapi/builtin/demo/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from . import models, routes
|
||||||
51
src/ohmyapi/builtin/demo/models.py
Normal file
51
src/ohmyapi/builtin/demo/models.py
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
from datetime import datetime
|
||||||
|
from decimal import Decimal
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
|
from ohmyapi_auth.models import User
|
||||||
|
|
||||||
|
from ohmyapi.db import Model, field
|
||||||
|
|
||||||
|
|
||||||
|
class Team(Model):
|
||||||
|
id: UUID = field.data.UUIDField(primary_key=True)
|
||||||
|
name: str = field.TextField()
|
||||||
|
members: field.ManyToManyRelation[User] = field.ManyToManyField(
|
||||||
|
"ohmyapi_auth.User",
|
||||||
|
related_name="tournament_teams",
|
||||||
|
through="user_tournament_teams",
|
||||||
|
)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
|
|
||||||
|
|
||||||
|
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: field.ForeignKeyRelation[Tournament] = field.ForeignKeyField(
|
||||||
|
"ohmyapi_demo.Tournament",
|
||||||
|
related_name="events",
|
||||||
|
)
|
||||||
|
participants: field.ManyToManyRelation[Team] = field.ManyToManyField(
|
||||||
|
"ohmyapi_demo.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)
|
||||||
|
|
||||||
|
class Schema:
|
||||||
|
exclude = ["tournament_id"]
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.name
|
||||||
53
src/ohmyapi/builtin/demo/routes.py
Normal file
53
src/ohmyapi/builtin/demo/routes.py
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
from typing import List
|
||||||
|
|
||||||
|
from ohmyapi.db.exceptions import DoesNotExist
|
||||||
|
from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
|
||||||
|
|
||||||
|
from . import models
|
||||||
|
|
||||||
|
# Expose your app's routes via `router = fastapi.APIRouter`.
|
||||||
|
# Use prefixes wisely to avoid cross-app namespace-collisions.
|
||||||
|
# Tags improve the UX of the OpenAPI docs at /docs.
|
||||||
|
router = APIRouter(prefix="/tournament")
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/", tags=["tournament"], response_model=List[models.Tournament.Schema()])
|
||||||
|
async def list():
|
||||||
|
"""List all tournaments."""
|
||||||
|
return await models.Tournament.Schema().from_queryset(models.Tournament.all())
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/", tags=["tournament"], status_code=HTTPStatus.CREATED)
|
||||||
|
async def post(tournament: models.Tournament.Schema(readonly=True)):
|
||||||
|
"""Create tournament."""
|
||||||
|
return await models.Tournament.Schema().from_queryset(
|
||||||
|
models.Tournament.create(**tournament.model_dump())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}", tags=["tournament"], response_model=models.Tournament.Schema())
|
||||||
|
async def get(id: str):
|
||||||
|
"""Get tournament by id."""
|
||||||
|
return await models.Tournament.Schema().from_queryset(models.Tournament.get(id=id))
|
||||||
|
|
||||||
|
|
||||||
|
@router.put(
|
||||||
|
"/{id}",
|
||||||
|
tags=["tournament"],
|
||||||
|
response_model=models.Tournament.Schema.model,
|
||||||
|
status_code=HTTPStatus.ACCEPTED,
|
||||||
|
)
|
||||||
|
async def put(tournament: models.Tournament.Schema.model):
|
||||||
|
"""Update tournament."""
|
||||||
|
return await models.Tournament.Schema().from_queryset(
|
||||||
|
models.Tournament.update(**tournament.model_dump())
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}", status_code=HTTPStatus.ACCEPTED, tags=["tournament"])
|
||||||
|
async def delete(id: str):
|
||||||
|
try:
|
||||||
|
tournament = await models.Tournament.get(id=id)
|
||||||
|
return await tournament.delete()
|
||||||
|
except DoesNotExist:
|
||||||
|
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="not found")
|
||||||
|
|
@ -1,27 +1,45 @@
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import atexit
|
||||||
import importlib
|
import importlib
|
||||||
import sys
|
import sys
|
||||||
|
from getpass import getpass
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
import typer
|
import typer
|
||||||
import uvicorn
|
import uvicorn
|
||||||
|
|
||||||
from getpass import getpass
|
from ohmyapi.core import runtime, scaffolding
|
||||||
from ohmyapi.core import scaffolding, runtime
|
from ohmyapi.core.logging import setup_logging
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
app = typer.Typer(help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM.")
|
logger = setup_logging()
|
||||||
banner = """OhMyAPI Shell | Project: {project_name}"""
|
|
||||||
|
app = typer.Typer(
|
||||||
|
help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM."
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def startproject(name: str):
|
def startproject(name: str):
|
||||||
"""Create a new OhMyAPI project in the given directory"""
|
"""Create a new OhMyAPI project in the given directory."""
|
||||||
scaffolding.startproject(name)
|
scaffolding.startproject(name)
|
||||||
|
logger.info(f"✅ Project '{name}' created successfully.")
|
||||||
|
logger.info(f"🔧 Next, configure your project in {name}/settings.py")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def startapp(app_name: str, root: str = "."):
|
def startapp(app_name: str, root: str = "."):
|
||||||
"""Create a new app with the given name in your OhMyAPI project"""
|
"""Create a new app with the given name in your OhMyAPI project."""
|
||||||
scaffolding.startapp(app_name, root)
|
scaffolding.startapp(app_name, root)
|
||||||
|
print(f"✅ App '{app_name}' created in project '{root}' successfully.")
|
||||||
|
print(f"🔧 Remember to add '{app_name}' to your INSTALLED_APPS!")
|
||||||
|
|
||||||
|
|
||||||
|
@app.command()
|
||||||
|
def dockerize(root: str = "."):
|
||||||
|
"""Create template Dockerfile and docker-compose.yml."""
|
||||||
|
scaffolding.copy_static("docker", root)
|
||||||
|
logger.info(f"✅ Templates created successfully.")
|
||||||
|
logger.info(f"🔧 Next, run `docker compose up -d --build`")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|
@ -31,36 +49,58 @@ def serve(root: str = ".", host="127.0.0.1", port=8000):
|
||||||
"""
|
"""
|
||||||
project_path = Path(root)
|
project_path = Path(root)
|
||||||
project = runtime.Project(project_path)
|
project = runtime.Project(project_path)
|
||||||
app_instance = project.app()
|
app_instance = project.configure_app(project.app())
|
||||||
uvicorn.run(app_instance, host=host, port=int(port), reload=False)
|
uvicorn.run(app_instance, host=host, port=int(port), reload=False, log_config=None)
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def shell(root: str = "."):
|
def shell(root: str = "."):
|
||||||
"""
|
"""An interactive shell with your loaded project runtime."""
|
||||||
Launch an interactive IPython shell with the project and apps loaded.
|
|
||||||
"""
|
|
||||||
project_path = Path(root).resolve()
|
project_path = Path(root).resolve()
|
||||||
project = runtime.Project(project_path)
|
project = runtime.Project(project_path)
|
||||||
|
|
||||||
|
banner = f"""
|
||||||
|
OhMyAPI Project Shell: {getattr(project.settings, 'PROJECT_NAME', 'MyProject')}
|
||||||
|
Find your loaded project singleton via identifier: `p`; i.e.: `p.apps`
|
||||||
|
"""
|
||||||
|
|
||||||
|
async def init_and_cleanup():
|
||||||
|
try:
|
||||||
|
await project.init_orm()
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Failed to initialize ORM: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def cleanup():
|
||||||
|
try:
|
||||||
|
await project.close_orm()
|
||||||
|
print("Tortoise ORM closed successfully.")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"Error closing ORM: {e}")
|
||||||
|
|
||||||
|
loop = asyncio.new_event_loop()
|
||||||
|
asyncio.set_event_loop(loop)
|
||||||
|
loop.run_until_complete(init_and_cleanup())
|
||||||
|
|
||||||
|
# Prepare shell vars that are to be immediately available
|
||||||
|
shell_vars = {"p": project}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from IPython import start_ipython
|
from IPython import start_ipython
|
||||||
shell_vars = {
|
|
||||||
"settings": project.settings,
|
|
||||||
"project": Path(project_path).resolve(),
|
|
||||||
}
|
|
||||||
from traitlets.config.loader import Config
|
from traitlets.config.loader import Config
|
||||||
|
|
||||||
c = Config()
|
c = Config()
|
||||||
c.TerminalIPythonApp.display_banner = True
|
c.TerminalIPythonApp.display_banner = True
|
||||||
c.TerminalInteractiveShell.banner1 = banner.format(**{
|
c.TerminalInteractiveShell.banner2 = banner
|
||||||
"project_name": f"{f'{project.settings.PROJECT_NAME} ' if getattr(project.settings, 'PROJECT_NAME', '') else ''}[{Path(project_path).resolve()}]",
|
|
||||||
})
|
|
||||||
c.TerminalInteractiveShell.banner2 = " "
|
|
||||||
start_ipython(argv=[], user_ns=shell_vars, config=c)
|
start_ipython(argv=[], user_ns=shell_vars, config=c)
|
||||||
except ImportError:
|
except ImportError:
|
||||||
typer.echo("IPython is not installed. Falling back to built-in Python shell.")
|
|
||||||
import code
|
import code
|
||||||
code.interact(local={"settings": project.settings})
|
|
||||||
|
code.interact(local=shell_vars, banner=banner)
|
||||||
|
finally:
|
||||||
|
loop.run_until_complete(cleanup())
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|
@ -75,6 +115,8 @@ def makemigrations(app: str = "*", name: str = "auto", root: str = "."):
|
||||||
asyncio.run(project.makemigrations(app_label=app, name=name))
|
asyncio.run(project.makemigrations(app_label=app, name=name))
|
||||||
else:
|
else:
|
||||||
asyncio.run(project.makemigrations(app_label=app, name=name))
|
asyncio.run(project.makemigrations(app_label=app, name=name))
|
||||||
|
logger.info(f"✅ Migrations created successfully.")
|
||||||
|
logger.info(f"🔧 To migrate the DB, run `ohmyapi migrate`, next.")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
|
|
@ -89,26 +131,47 @@ def migrate(app: str = "*", root: str = "."):
|
||||||
asyncio.run(project.migrate(app))
|
asyncio.run(project.migrate(app))
|
||||||
else:
|
else:
|
||||||
asyncio.run(project.migrate(app))
|
asyncio.run(project.migrate(app))
|
||||||
|
logger.info(f"✅ Migrations ran successfully.")
|
||||||
|
|
||||||
|
|
||||||
@app.command()
|
@app.command()
|
||||||
def createsuperuser(root: str = "."):
|
def createsuperuser(root: str = "."):
|
||||||
|
"""Create a superuser in the DB.
|
||||||
|
|
||||||
|
This requires the presence of `ohmyapi_auth` in your INSTALLED_APPS to work.
|
||||||
|
"""
|
||||||
project_path = Path(root).resolve()
|
project_path = Path(root).resolve()
|
||||||
project = runtime.Project(project_path)
|
project = runtime.Project(project_path)
|
||||||
if not project.is_app_installed("ohmyapi_auth"):
|
if not project.is_app_installed("ohmyapi_auth"):
|
||||||
print("Auth app not installed! Please add 'ohmyapi_auth' to your INSTALLED_APPS.")
|
print(
|
||||||
|
"Auth app not installed! Please add 'ohmyapi_auth' to your INSTALLED_APPS."
|
||||||
|
)
|
||||||
return
|
return
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
|
||||||
import ohmyapi_auth
|
import ohmyapi_auth
|
||||||
|
|
||||||
|
email = input("E-Mail: ")
|
||||||
username = input("Username: ")
|
username = input("Username: ")
|
||||||
password = getpass("Password: ")
|
password1, password2 = "foo", "bar"
|
||||||
|
while password1 != password2:
|
||||||
|
password1 = getpass("Password: ")
|
||||||
|
password2 = getpass("Repeat Password: ")
|
||||||
|
if password1 != password2:
|
||||||
|
print("Passwords didn't match!")
|
||||||
|
user = ohmyapi_auth.models.User(
|
||||||
|
username=username, is_staff=True, is_admin=True
|
||||||
|
)
|
||||||
|
user.set_email(email)
|
||||||
|
user.set_password(password1)
|
||||||
|
|
||||||
|
async def _run():
|
||||||
|
await project.init_orm()
|
||||||
user = ohmyapi_auth.models.User(username=username, is_staff=True, is_admin=True)
|
user = ohmyapi_auth.models.User(username=username, is_staff=True, is_admin=True)
|
||||||
user.set_password(password)
|
user.set_email(email)
|
||||||
asyncio.run(project.init_orm())
|
user.set_password(password1)
|
||||||
asyncio.run(user.save())
|
await user.save()
|
||||||
asyncio.run(project.close_orm())
|
await project.close_orm()
|
||||||
|
|
||||||
def main():
|
|
||||||
app()
|
|
||||||
|
|
||||||
|
asyncio.run(_run())
|
||||||
|
|
|
||||||
38
src/ohmyapi/core/logging.py
Normal file
38
src/ohmyapi/core/logging.py
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def setup_logging():
|
||||||
|
"""Configure unified logging for ohmyapi + FastAPI/Uvicorn."""
|
||||||
|
log_level = os.getenv("OHMYAPI_LOG_LEVEL", "INFO").upper()
|
||||||
|
level = getattr(logging, log_level, logging.INFO)
|
||||||
|
|
||||||
|
# Root logger (affects FastAPI, uvicorn, etc.)
|
||||||
|
logging.basicConfig(
|
||||||
|
level=level,
|
||||||
|
format="[%(asctime)s] [%(levelname)s] %(name)s: %(message)s",
|
||||||
|
datefmt="%Y-%m-%d %H:%M:%S",
|
||||||
|
handlers=[
|
||||||
|
logging.StreamHandler(sys.stdout)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Separate ohmyapi logger (optional)
|
||||||
|
logger = logging.getLogger("ohmyapi")
|
||||||
|
|
||||||
|
# Direct warnings/errors to stderr
|
||||||
|
class LevelFilter(logging.Filter):
|
||||||
|
def filter(self, record):
|
||||||
|
# Send warnings+ to stderr, everything else to stdout
|
||||||
|
if record.levelno >= logging.WARNING:
|
||||||
|
record.stream = sys.stderr
|
||||||
|
else:
|
||||||
|
record.stream = sys.stdout
|
||||||
|
return True
|
||||||
|
|
||||||
|
for handler in logger.handlers:
|
||||||
|
handler.addFilter(LevelFilter())
|
||||||
|
|
||||||
|
logger.setLevel(level)
|
||||||
|
return logger
|
||||||
|
|
@ -1,91 +1,38 @@
|
||||||
# ohmyapi/core/runtime.py
|
# ohmyapi/core/runtime.py
|
||||||
import copy
|
|
||||||
import importlib
|
import importlib
|
||||||
import importlib.util
|
import importlib.util
|
||||||
|
import json
|
||||||
import pkgutil
|
import pkgutil
|
||||||
import sys
|
import sys
|
||||||
|
from http import HTTPStatus
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Dict, List, Optional
|
from types import ModuleType
|
||||||
|
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from aerich import Command as AerichCommand
|
from aerich import Command as AerichCommand
|
||||||
from aerich.exceptions import NotInitedError
|
from aerich.exceptions import NotInitedError
|
||||||
|
from fastapi import APIRouter, FastAPI
|
||||||
from tortoise import Tortoise
|
from tortoise import Tortoise
|
||||||
from fastapi import FastAPI, APIRouter
|
|
||||||
|
from ohmyapi.core.logging import setup_logging
|
||||||
from ohmyapi.db.model import Model
|
from ohmyapi.db.model import Model
|
||||||
|
|
||||||
|
logger = setup_logging()
|
||||||
class App:
|
|
||||||
"""App container holding runtime data like detected models and routes."""
|
|
||||||
|
|
||||||
def __init__(self, project: "OhMyAPI Project", name: str):
|
|
||||||
self.project = project
|
|
||||||
self.name = name
|
|
||||||
|
|
||||||
# The list of module paths (e.g. "ohmyapi_auth.models") for Tortoise and Aerich
|
|
||||||
self.model_modules: List[str] = []
|
|
||||||
|
|
||||||
# The APIRouter
|
|
||||||
self.router: Optional[APIRouter] = None
|
|
||||||
|
|
||||||
# Import the app, so its __init__.py runs.
|
|
||||||
importlib.import_module(self.name)
|
|
||||||
|
|
||||||
# Load the models
|
|
||||||
try:
|
|
||||||
models_mod = importlib.import_module(f"{self.name}.models")
|
|
||||||
self.model_modules.append(f"{self.name}.models")
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Locate the APIRouter
|
|
||||||
try:
|
|
||||||
routes_mod = importlib.import_module(f"{self.name}.routes")
|
|
||||||
router = getattr(routes_mod, "router", None)
|
|
||||||
if isinstance(router, APIRouter):
|
|
||||||
self.router = router
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
out = ""
|
|
||||||
out += f"App: {self.name}\n"
|
|
||||||
out += f"Models:\n"
|
|
||||||
for model in self.models:
|
|
||||||
out += f" - {model.__name__}\n"
|
|
||||||
out += "Routes:\n"
|
|
||||||
for route in (self.routes or []):
|
|
||||||
out += f" - {route}\n"
|
|
||||||
return out
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return self.__repr__()
|
|
||||||
|
|
||||||
@property
|
|
||||||
def models(self) -> List[Model]:
|
|
||||||
models: List[Model] = []
|
|
||||||
for mod in self.model_modules:
|
|
||||||
models_mod = importlib.import_module(mod)
|
|
||||||
for obj in models_mod.__dict__.values():
|
|
||||||
if isinstance(obj, type) and getattr(obj, "_meta", None) is not None and obj.__name__ != 'Model':
|
|
||||||
models.append(obj)
|
|
||||||
return models
|
|
||||||
|
|
||||||
@property
|
|
||||||
def routes(self):
|
|
||||||
return self.router.routes
|
|
||||||
|
|
||||||
|
|
||||||
class Project:
|
class Project:
|
||||||
"""
|
"""
|
||||||
Project runtime loader + Tortoise/Aerich integration.
|
Project runtime loader + Tortoise/Aerich integration.
|
||||||
|
|
||||||
- injects builtin apps as ohmyapi_<name>
|
- aliases builtin apps as ohmyapi_<name>
|
||||||
- builds unified tortoise config for runtime
|
- loads all INSTALLED_APPS into scope
|
||||||
|
- builds unified tortoise config for ORM runtime
|
||||||
- provides makemigrations/migrate methods using Aerich Command API
|
- provides makemigrations/migrate methods using Aerich Command API
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, project_path: str):
|
def __init__(self, project_path: str):
|
||||||
|
logger.debug(f"Loading project: {project_path}")
|
||||||
self.project_path = Path(project_path).resolve()
|
self.project_path = Path(project_path).resolve()
|
||||||
self._apps: Dict[str, App] = {}
|
self._apps: Dict[str, App] = {}
|
||||||
self.migrations_dir = self.project_path / "migrations"
|
self.migrations_dir = self.project_path / "migrations"
|
||||||
|
|
@ -93,8 +40,16 @@ class Project:
|
||||||
if str(self.project_path) not in sys.path:
|
if str(self.project_path) not in sys.path:
|
||||||
sys.path.insert(0, str(self.project_path))
|
sys.path.insert(0, str(self.project_path))
|
||||||
|
|
||||||
# Pre-register builtin apps as ohmyapi_<name>.
|
# Load settings.py
|
||||||
# This makes all builtin apps easily loadable via f"ohmyapi_{app_name}".
|
try:
|
||||||
|
self.settings = importlib.import_module("settings")
|
||||||
|
except Exception as e:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Failed to import project settings from {self.project_path}"
|
||||||
|
) from e
|
||||||
|
|
||||||
|
# Alias builtin apps as ohmyapi_<name>.
|
||||||
|
# We need this, because Tortoise app-names may not include dots `.`.
|
||||||
spec = importlib.util.find_spec("ohmyapi.builtin")
|
spec = importlib.util.find_spec("ohmyapi.builtin")
|
||||||
if spec and spec.submodule_search_locations:
|
if spec and spec.submodule_search_locations:
|
||||||
for _, modname, _ in pkgutil.iter_modules(spec.submodule_search_locations):
|
for _, modname, _ in pkgutil.iter_modules(spec.submodule_search_locations):
|
||||||
|
|
@ -104,16 +59,12 @@ class Project:
|
||||||
orig = importlib.import_module(full)
|
orig = importlib.import_module(full)
|
||||||
sys.modules[alias] = orig
|
sys.modules[alias] = orig
|
||||||
try:
|
try:
|
||||||
sys.modules[f"{alias}.models"] = importlib.import_module(f"{full}.models")
|
sys.modules[f"{alias}.models"] = importlib.import_module(
|
||||||
|
f"{full}.models"
|
||||||
|
)
|
||||||
except ModuleNotFoundError:
|
except ModuleNotFoundError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# Load settings.py
|
|
||||||
try:
|
|
||||||
self.settings = importlib.import_module("settings")
|
|
||||||
except Exception as e:
|
|
||||||
raise RuntimeError(f"Failed to import project settings from {self.project_path}") from e
|
|
||||||
|
|
||||||
# Load installed apps
|
# Load installed apps
|
||||||
for app_name in getattr(self.settings, "INSTALLED_APPS", []):
|
for app_name in getattr(self.settings, "INSTALLED_APPS", []):
|
||||||
self._apps[app_name] = App(self, name=app_name)
|
self._apps[app_name] = App(self, name=app_name)
|
||||||
|
|
@ -125,23 +76,39 @@ class Project:
|
||||||
def is_app_installed(self, name: str) -> bool:
|
def is_app_installed(self, name: str) -> bool:
|
||||||
return name in getattr(self.settings, "INSTALLED_APPS", [])
|
return name in getattr(self.settings, "INSTALLED_APPS", [])
|
||||||
|
|
||||||
def app(self, generate_schemas: bool = False) -> FastAPI:
|
def app(self,
|
||||||
|
docs_url: str = "/docs",
|
||||||
|
) -> FastAPI:
|
||||||
"""
|
"""
|
||||||
Create a FastAPI app, attach all APIRouters from registered apps,
|
Create and return a FastAPI app.
|
||||||
and register ORM lifecycle event handlers.
|
|
||||||
"""
|
"""
|
||||||
app = FastAPI(title=getattr(self.settings, "PROJECT_NAME", "OhMyAPI Project"))
|
import ohmyapi
|
||||||
|
return FastAPI(
|
||||||
|
title=getattr(self.settings, "PROJECT_NAME", "OhMyAPI Project"),
|
||||||
|
description=getattr(self.settings, "PROJECT_DESCRIPTION", ""),
|
||||||
|
docs_url=getattr(self.settings, "DOCS_URL", "/docs"),
|
||||||
|
version=ohmyapi.__VERSION__,
|
||||||
|
)
|
||||||
|
|
||||||
# Attach routers from apps
|
def configure_app(self, app: FastAPI) -> FastAPI:
|
||||||
|
"""
|
||||||
|
Attach project middlewares and routes and event handlers to given
|
||||||
|
FastAPI instance.
|
||||||
|
"""
|
||||||
|
app.router.prefix = getattr(self.settings, "API_PREFIX", "")
|
||||||
|
# Attach project middlewares and routes.
|
||||||
for app_name, app_def in self._apps.items():
|
for app_name, app_def in self._apps.items():
|
||||||
if app_def.router:
|
for middleware, kwargs in app_def.middlewares:
|
||||||
app.include_router(app_def.router)
|
app.add_middleware(middleware, **kwargs)
|
||||||
|
for router in app_def.routers:
|
||||||
|
app.include_router(router)
|
||||||
|
|
||||||
# Startup / shutdown events
|
# Initialize ORM on startup
|
||||||
@app.on_event("startup")
|
@app.on_event("startup")
|
||||||
async def _startup():
|
async def _startup():
|
||||||
await self.init_orm(generate_schemas=generate_schemas)
|
await self.init_orm(generate_schemas=False)
|
||||||
|
|
||||||
|
# Close ORM on shutdown
|
||||||
@app.on_event("shutdown")
|
@app.on_event("shutdown")
|
||||||
async def _shutdown():
|
async def _shutdown():
|
||||||
await self.close_orm()
|
await self.close_orm()
|
||||||
|
|
@ -162,34 +129,43 @@ class Project:
|
||||||
}
|
}
|
||||||
|
|
||||||
for app_name, app in self._apps.items():
|
for app_name, app in self._apps.items():
|
||||||
modules = list(dict.fromkeys(app.model_modules))
|
modules = list(app._models.keys())
|
||||||
if modules:
|
if modules:
|
||||||
config["apps"][app_name] = {"models": modules, "default_connection": "default"}
|
config["apps"][app_name] = {
|
||||||
|
"models": modules,
|
||||||
|
"default_connection": "default",
|
||||||
|
}
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
def build_aerich_command(self, app_label: str, db_url: Optional[str] = None) -> AerichCommand:
|
def build_aerich_command(
|
||||||
# Resolve label to flat_label
|
self, app_label: str, db_url: Optional[str] = None
|
||||||
if app_label in self._apps:
|
) -> AerichCommand:
|
||||||
flat_label = app_label
|
"""
|
||||||
else:
|
Build Aerich command for app with given app_label.
|
||||||
candidate = app_label.replace(".", "_")
|
|
||||||
if candidate in self._apps:
|
Aerich needs to see only the app of interest, but with the extra model
|
||||||
flat_label = candidate
|
"aerich.models".
|
||||||
else:
|
"""
|
||||||
|
if app_label not in self._apps:
|
||||||
raise RuntimeError(f"App '{app_label}' is not registered")
|
raise RuntimeError(f"App '{app_label}' is not registered")
|
||||||
|
|
||||||
# Get a fresh copy of the config (without aerich.models anywhere)
|
# Get a fresh copy of the config (without aerich.models anywhere)
|
||||||
tortoise_cfg = copy.deepcopy(self.build_tortoise_config(db_url=db_url))
|
tortoise_cfg = self.build_tortoise_config(db_url=db_url)
|
||||||
|
|
||||||
|
# Prevent leaking other app's models to Aerich.
|
||||||
|
if app_label in tortoise_cfg["apps"].keys():
|
||||||
|
tortoise_cfg["apps"] = {app_label: tortoise_cfg["apps"][app_label]}
|
||||||
|
else:
|
||||||
|
tortoise_cfg["apps"] = {app_label: {"default_connection": "default", "models": []}}
|
||||||
|
|
||||||
# Append aerich.models to the models list of the target app only
|
# Append aerich.models to the models list of the target app only
|
||||||
if flat_label in tortoise_cfg["apps"]:
|
tortoise_cfg["apps"][app_label]["models"].append("aerich.models")
|
||||||
tortoise_cfg["apps"][flat_label]["models"].append("aerich.models")
|
|
||||||
|
|
||||||
return AerichCommand(
|
return AerichCommand(
|
||||||
tortoise_config=tortoise_cfg,
|
tortoise_config=tortoise_cfg,
|
||||||
app=flat_label,
|
app=app_label,
|
||||||
location=str(self.migrations_dir)
|
location=str(self.migrations_dir),
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- ORM lifecycle ---
|
# --- ORM lifecycle ---
|
||||||
|
|
@ -204,7 +180,9 @@ class Project:
|
||||||
await Tortoise.close_connections()
|
await Tortoise.close_connections()
|
||||||
|
|
||||||
# --- Migration helpers ---
|
# --- Migration helpers ---
|
||||||
async def makemigrations(self, app_label: str, name: str = "auto", db_url: Optional[str] = None) -> None:
|
async def makemigrations(
|
||||||
|
self, app_label: str, name: str = "auto", db_url: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
cmd = self.build_aerich_command(app_label, db_url=db_url)
|
cmd = self.build_aerich_command(app_label, db_url=db_url)
|
||||||
async with cmd as c:
|
async with cmd as c:
|
||||||
await c.init()
|
await c.init()
|
||||||
|
|
@ -218,7 +196,9 @@ class Project:
|
||||||
await c.init_db(safe=True)
|
await c.init_db(safe=True)
|
||||||
await c.migrate(name=name)
|
await c.migrate(name=name)
|
||||||
|
|
||||||
async def migrate(self, app_label: Optional[str] = None, db_url: Optional[str] = None) -> None:
|
async def migrate(
|
||||||
|
self, app_label: Optional[str] = None, db_url: Optional[str] = None
|
||||||
|
) -> None:
|
||||||
labels: List[str]
|
labels: List[str]
|
||||||
if app_label:
|
if app_label:
|
||||||
if app_label in self._apps:
|
if app_label in self._apps:
|
||||||
|
|
@ -244,3 +224,195 @@ class Project:
|
||||||
# No migrations yet, initialize then retry upgrade
|
# No migrations yet, initialize then retry upgrade
|
||||||
await c.init_db(safe=True)
|
await c.init_db(safe=True)
|
||||||
await c.upgrade()
|
await c.upgrade()
|
||||||
|
|
||||||
|
|
||||||
|
class App:
|
||||||
|
"""App container holding runtime data like detected models and routes."""
|
||||||
|
|
||||||
|
def __init__(self, project: Project, name: str):
|
||||||
|
self.project = project
|
||||||
|
self.name = name
|
||||||
|
|
||||||
|
# Reference to this app's models modules. Tortoise needs to know the
|
||||||
|
# modules where to lookup models for this app.
|
||||||
|
self._models: Dict[str, ModuleType] = {}
|
||||||
|
|
||||||
|
# Reference to this app's routes modules.
|
||||||
|
self._routers: Dict[str, ModuleType] = {}
|
||||||
|
|
||||||
|
# Reference to this apps middlewares.
|
||||||
|
self._middlewares: List[Tuple[Any, Dict[str, Any]]] = []
|
||||||
|
|
||||||
|
# Import the app, so its __init__.py runs.
|
||||||
|
mod: ModuleType = importlib.import_module(name)
|
||||||
|
|
||||||
|
logger.debug(f"Loading app: {self.name}")
|
||||||
|
self.__load_models(f"{self.name}.models")
|
||||||
|
self.__load_routes(f"{self.name}.routes")
|
||||||
|
self.__load_middlewares(f"{self.name}.middlewares")
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return json.dumps(self.dict(), indent=2)
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return self.__repr__()
|
||||||
|
|
||||||
|
def __load_models(self, mod_name: str):
|
||||||
|
"""
|
||||||
|
Recursively scan through a module and collect all models.
|
||||||
|
If the module is a package, iterate through its submodules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# An app may come without any models.
|
||||||
|
try:
|
||||||
|
importlib.import_module(mod_name)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Acoid duplicates.
|
||||||
|
visited: set[str] = set()
|
||||||
|
|
||||||
|
def walk(mod_name: str):
|
||||||
|
mod = importlib.import_module(mod_name)
|
||||||
|
if mod_name in visited:
|
||||||
|
return
|
||||||
|
visited.add(mod_name)
|
||||||
|
|
||||||
|
for name, value in vars(mod).copy().items():
|
||||||
|
if (
|
||||||
|
isinstance(value, type)
|
||||||
|
and issubclass(value, Model)
|
||||||
|
and not name == Model.__name__
|
||||||
|
):
|
||||||
|
# monkey-patch __module__ to point to well-known aliases
|
||||||
|
value.__module__ = value.__module__.replace("ohmyapi.builtin.", "ohmyapi_")
|
||||||
|
if value.__module__.startswith(mod_name):
|
||||||
|
self._models[mod_name] = self._models.get(mod_name, []) + [value]
|
||||||
|
logger.debug(f" - Model: {mod_name} -> {name}")
|
||||||
|
|
||||||
|
# if it's a package, recurse into submodules
|
||||||
|
if hasattr(mod, "__path__"):
|
||||||
|
for _, subname, _ in pkgutil.iter_modules(
|
||||||
|
mod.__path__, mod.__name__ + "."
|
||||||
|
):
|
||||||
|
walk(subname)
|
||||||
|
|
||||||
|
# Walk the walk.
|
||||||
|
walk(mod_name)
|
||||||
|
|
||||||
|
def __load_routes(self, mod_name: str):
|
||||||
|
"""
|
||||||
|
Recursively scan through a module and collect all APIRouters.
|
||||||
|
If the module is a package, iterate through all its submodules.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# An app may come without any routes.
|
||||||
|
try:
|
||||||
|
importlib.import_module(mod_name)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Avoid duplicates.
|
||||||
|
visited: set[str] = set()
|
||||||
|
|
||||||
|
def walk(mod_name: str):
|
||||||
|
mod = importlib.import_module(mod_name)
|
||||||
|
if mod.__name__ in visited:
|
||||||
|
return
|
||||||
|
visited.add(mod.__name__)
|
||||||
|
|
||||||
|
for name, value in vars(mod).copy().items():
|
||||||
|
if isinstance(value, APIRouter) and not name == APIRouter.__name__:
|
||||||
|
self._routers[mod_name] = self._routers.get(mod_name, []) + [value]
|
||||||
|
logger.debug(f" - Router: {mod_name} -> {name} -> {value.routes}")
|
||||||
|
|
||||||
|
# if it's a package, recurse into submodules
|
||||||
|
if hasattr(mod, "__path__"):
|
||||||
|
for _, subname, _ in pkgutil.iter_modules(
|
||||||
|
mod.__path__, mod.__name__ + "."
|
||||||
|
):
|
||||||
|
walk(subname)
|
||||||
|
|
||||||
|
# Walk the walk.
|
||||||
|
walk(mod_name)
|
||||||
|
|
||||||
|
def __load_middlewares(self, mod_name):
|
||||||
|
try:
|
||||||
|
mod = importlib.import_module(mod_name)
|
||||||
|
except ModuleNotFoundError:
|
||||||
|
return
|
||||||
|
|
||||||
|
installer = getattr(mod, "install", None)
|
||||||
|
if installer is not None:
|
||||||
|
for middleware in installer():
|
||||||
|
self._middlewares.append(middleware)
|
||||||
|
|
||||||
|
def __serialize_route(self, route):
|
||||||
|
"""
|
||||||
|
Convert APIRoute to JSON-serializable dict.
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
"path": route.path,
|
||||||
|
"method": list(route.methods)[0],
|
||||||
|
"endpoint": f"{route.endpoint.__module__}.{route.endpoint.__name__}",
|
||||||
|
}
|
||||||
|
|
||||||
|
def __serialize_router(self):
|
||||||
|
return [self.__serialize_route(route) for route in self.routes]
|
||||||
|
|
||||||
|
def __serialize_middleware(self):
|
||||||
|
out = []
|
||||||
|
for m in self.middlewares:
|
||||||
|
out.append((m[0].__name__, m[1]))
|
||||||
|
return out
|
||||||
|
|
||||||
|
@property
|
||||||
|
def models(self) -> List[ModuleType]:
|
||||||
|
"""
|
||||||
|
Return a list of all loaded models.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for module in self._models:
|
||||||
|
for model in self._models[module]:
|
||||||
|
out.append(model)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@property
|
||||||
|
def routers(self):
|
||||||
|
out = []
|
||||||
|
for routes_mod in self._routers:
|
||||||
|
for r in self._routers[routes_mod]:
|
||||||
|
out.append(r)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@property
|
||||||
|
def routes(self):
|
||||||
|
"""
|
||||||
|
Return an APIRouter with all loaded routes.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for r in self.routers:
|
||||||
|
out.extend(r.routes)
|
||||||
|
return out
|
||||||
|
|
||||||
|
@property
|
||||||
|
def middlewares(self):
|
||||||
|
"""Returns the list of this app's middlewares."""
|
||||||
|
return self._middlewares
|
||||||
|
|
||||||
|
def dict(self) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Convenience method for serializing the runtime data.
|
||||||
|
"""
|
||||||
|
# An app may come without any models
|
||||||
|
models = []
|
||||||
|
if f"{self.name}.models" in self._models:
|
||||||
|
models = [
|
||||||
|
f"{self.name}.{m.__name__}"
|
||||||
|
for m in self._models[f"{self.name}.models"]
|
||||||
|
]
|
||||||
|
return {
|
||||||
|
"models": models,
|
||||||
|
"middlewares": self.__serialize_middleware(),
|
||||||
|
"routes": self.__serialize_router(),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,37 +1,54 @@
|
||||||
import os
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
|
||||||
|
from ohmyapi.core.logging import setup_logging
|
||||||
|
|
||||||
|
import shutil
|
||||||
|
|
||||||
# Base templates directory
|
# Base templates directory
|
||||||
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
||||||
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
||||||
|
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
def render_template_file(template_path: Path, context: dict, output_path: Path):
|
def render_template_file(template_path: Path, context: dict, output_path: Path):
|
||||||
"""Render a single Jinja2 template file to disk."""
|
"""Render a single Jinja2 template file to disk."""
|
||||||
template = env.get_template(str(template_path.relative_to(TEMPLATE_DIR)).replace("\\", "/"))
|
template = env.get_template(
|
||||||
|
str(template_path.relative_to(TEMPLATE_DIR)).replace("\\", "/")
|
||||||
|
)
|
||||||
content = template.render(**context)
|
content = template.render(**context)
|
||||||
os.makedirs(output_path.parent, exist_ok=True)
|
output_path.parent.mkdir(exist_ok=True)
|
||||||
with open(output_path, "w", encoding="utf-8") as f:
|
with open(output_path, "w", encoding="utf-8") as f:
|
||||||
f.write(content)
|
f.write(content)
|
||||||
|
|
||||||
|
|
||||||
def render_template_dir(template_subdir: str, target_dir: Path, context: dict, subdir_name: str | None = None):
|
def render_template_dir(
|
||||||
|
template_subdir: str,
|
||||||
|
target_dir: Path,
|
||||||
|
context: dict,
|
||||||
|
subdir_name: str | None = None,
|
||||||
|
):
|
||||||
"""
|
"""
|
||||||
Recursively render all *.j2 templates from TEMPLATE_DIR/template_subdir into target_dir.
|
Recursively render all *.j2 templates from TEMPLATE_DIR/template_subdir into target_dir.
|
||||||
If subdir_name is given, files are placed inside target_dir/subdir_name.
|
If subdir_name is given, files are placed inside target_dir/subdir_name.
|
||||||
"""
|
"""
|
||||||
template_dir = TEMPLATE_DIR / template_subdir
|
template_dir = TEMPLATE_DIR / template_subdir
|
||||||
for root, _, files in os.walk(template_dir):
|
for root, _, files in template_dir.walk():
|
||||||
root_path = Path(root)
|
root_path = Path(root)
|
||||||
rel_root = root_path.relative_to(template_dir) # path relative to template_subdir
|
rel_root = root_path.relative_to(
|
||||||
|
template_dir
|
||||||
|
) # path relative to template_subdir
|
||||||
|
|
||||||
for f in files:
|
for f in files:
|
||||||
if not f.endswith(".j2"):
|
if not f.endswith(".j2"):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
template_rel_path = rel_root / f
|
template_rel_path = rel_root / f
|
||||||
output_rel_path = Path(*template_rel_path.parts).with_suffix("") # remove .j2
|
output_rel_path = Path(*template_rel_path.parts).with_suffix(
|
||||||
|
""
|
||||||
|
) # remove .j2
|
||||||
|
|
||||||
# optionally wrap in subdir_name
|
# optionally wrap in subdir_name
|
||||||
if subdir_name:
|
if subdir_name:
|
||||||
|
|
@ -42,20 +59,38 @@ def render_template_dir(template_subdir: str, target_dir: Path, context: dict, s
|
||||||
render_template_file(template_dir / template_rel_path, context, output_path)
|
render_template_file(template_dir / template_rel_path, context, output_path)
|
||||||
|
|
||||||
|
|
||||||
|
def copy_static(dir_name: str, target_dir: Path):
|
||||||
|
"""Statically copy all files from {TEMPLATE_DIR}/{dir_name} to target_dir."""
|
||||||
|
template_dir = TEMPLATE_DIR / dir_name
|
||||||
|
target_dir = Path(target_dir)
|
||||||
|
if not template_dir.exists():
|
||||||
|
logger.error(f"no templates found under: {dir_name}")
|
||||||
|
return
|
||||||
|
|
||||||
|
for root, _, files in template_dir.walk():
|
||||||
|
root_path = Path(root)
|
||||||
|
for file in files:
|
||||||
|
src = root_path / file
|
||||||
|
dst = target_dir / file
|
||||||
|
if dst.exists():
|
||||||
|
logger.warning(f"⛔ File exists, skipping: {dst}")
|
||||||
|
continue
|
||||||
|
shutil.copy(src, dst)
|
||||||
|
|
||||||
def startproject(name: str):
|
def startproject(name: str):
|
||||||
"""Create a new project: flat structure, all project templates go into <name>/"""
|
"""Create a new project: flat structure, all project templates go into <name>/"""
|
||||||
target_dir = Path(name).resolve()
|
target_dir = Path(name).resolve()
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
target_dir.mkdir(exist_ok=True)
|
||||||
render_template_dir("project", target_dir, {"project_name": name})
|
render_template_dir("project", target_dir, {"project_name": name})
|
||||||
print(f"✅ Project '{name}' created successfully.")
|
|
||||||
print(f"🔧 Next, configure your project in {target_dir / 'settings.py'}")
|
|
||||||
|
|
||||||
|
|
||||||
def startapp(name: str, project: str):
|
def startapp(name: str, project: str):
|
||||||
"""Create a new app inside a project: templates go into <project_dir>/<name>/"""
|
"""Create a new app inside a project: templates go into <project_dir>/<name>/"""
|
||||||
target_dir = Path(project)
|
target_dir = Path(project)
|
||||||
os.makedirs(target_dir, exist_ok=True)
|
target_dir.mkdir(exist_ok=True)
|
||||||
render_template_dir("app", target_dir, {"project_name": project, "app_name": name}, subdir_name=name)
|
render_template_dir(
|
||||||
print(f"✅ App '{name}' created in project '{target_dir}' successfully.")
|
"app",
|
||||||
print(f"🔧 Remember to add '{name}' to your INSTALLED_APPS!")
|
target_dir,
|
||||||
|
{"project_name": target_dir.resolve().name, "app_name": name},
|
||||||
|
subdir_name=name,
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
from ohmyapi.db import Model, field
|
from ohmyapi.db import Model, field
|
||||||
|
|
||||||
|
|
||||||
class MyModel(Model):
|
# class MyModel(Model):
|
||||||
id: int = field.IntField(min=1, pk=True)
|
# id: int = field.IntField(min=1, pk=True)
|
||||||
...
|
# ...
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,44 @@
|
||||||
from ohmyapi.router import APIRouter
|
from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
|
||||||
|
|
||||||
from . import models
|
from . import models
|
||||||
|
|
||||||
router = APIRouter(prefix="/{{ app_name }}")
|
from typing import List
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
router = APIRouter(prefix="/{{ app_name }}", tags=['{{ app_name }}'])
|
||||||
|
|
||||||
|
|
||||||
@router.get("/")
|
@router.get("/", response_model=List)
|
||||||
def ping():
|
async def list():
|
||||||
return {
|
"""List all ..."""
|
||||||
"project": "{{ project_name }}",
|
return []
|
||||||
"app": "{{ app_name }}",
|
|
||||||
}
|
|
||||||
|
@router.post("/", status_code=HTTPStatus.CREATED)
|
||||||
|
async def post():
|
||||||
|
"""Create ..."""
|
||||||
|
raise HTTPException(status_code=HTTPStatus.IM_A_TEAPOT)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/{id}")
|
||||||
|
async def get(id: str):
|
||||||
|
"""Get single ..."""
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@router.put("/{id}")
|
||||||
|
async def put(id: str):
|
||||||
|
"""Update ..."""
|
||||||
|
return HTTPException(status_code=HTTPStatus.ACCEPTED)
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/{id}", status_code=HTTPStatus.ACCEPTED)
|
||||||
|
async def delete(id: str):
|
||||||
|
return HTTPException(status_code=HTTPStatus.ACCEPTED)
|
||||||
|
|
||||||
|
|
|
||||||
29
src/ohmyapi/core/templates/docker/Dockerfile
Normal file
29
src/ohmyapi/core/templates/docker/Dockerfile
Normal file
|
|
@ -0,0 +1,29 @@
|
||||||
|
FROM python:3.13-alpine
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
POETRY_HOME="/opt/poetry" \
|
||||||
|
POETRY_VIRTUALENVS_IN_PROJECT=true \
|
||||||
|
POETRY_NO_INTERACTION=1
|
||||||
|
|
||||||
|
RUN apk add --no-cache \
|
||||||
|
build-base \
|
||||||
|
curl \
|
||||||
|
git \
|
||||||
|
bash \
|
||||||
|
libffi-dev \
|
||||||
|
openssl-dev \
|
||||||
|
python3-dev \
|
||||||
|
musl-dev
|
||||||
|
|
||||||
|
RUN curl -sSL https://install.python-poetry.org | python3 -
|
||||||
|
ENV PATH="$POETRY_HOME/bin:$PATH"
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
COPY pyproject.toml poetry.lock* /app/
|
||||||
|
RUN poetry install
|
||||||
|
COPY . /app
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
CMD ["poetry", "run", "ohmyapi", "serve", "--host", "0.0.0.0"]
|
||||||
17
src/ohmyapi/core/templates/docker/docker-compose.yml
Normal file
17
src/ohmyapi/core/templates/docker/docker-compose.yml
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
services:
|
||||||
|
# db:
|
||||||
|
# image: postgres:latest
|
||||||
|
# restart: unless-stopped
|
||||||
|
# environment:
|
||||||
|
# POSTGRES_DB: ohmyapi
|
||||||
|
# POSTGRES_USER: ohmyapi
|
||||||
|
# POSTGRES_PASSWORD: ohmyapi
|
||||||
|
# ports:
|
||||||
|
# - 5432:5432
|
||||||
|
|
||||||
|
app:
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- 8000:8000
|
||||||
2
src/ohmyapi/core/templates/project/README.md.j2
Normal file
2
src/ohmyapi/core/templates/project/README.md.j2
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
# {{ project_name }}
|
||||||
|
|
||||||
|
|
@ -1,13 +1,26 @@
|
||||||
[tool.poetry]
|
[project]
|
||||||
name = "{{ project_name }}"
|
name = "{{ project_name }}"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
description = "OhMyAPI project"
|
description = "OhMyAPI project"
|
||||||
authors = ["You <you@example.com>"]
|
authors = [
|
||||||
|
{ name = "You", email = "you@you.tld" }
|
||||||
|
]
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
readme = "README.md"
|
||||||
|
license = { text = "MIT" }
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
dependencies = [
|
||||||
python = "^3.10"
|
# "asyncpg"
|
||||||
fastapi = "^0.115"
|
]
|
||||||
uvicorn = "^0.30"
|
|
||||||
tortoise-orm = "^0.20"
|
|
||||||
aerich = "^0.7"
|
|
||||||
|
|
||||||
|
[tool.poetry.group.dev.dependencies]
|
||||||
|
ipython = ">=9.5.0,<10.0.0"
|
||||||
|
|
||||||
|
[project.optional-dependencies]
|
||||||
|
auth = ["passlib", "pyjwt", "crypto", "argon2-cffi", "python-multipart"]
|
||||||
|
|
||||||
|
[tool.poetry]
|
||||||
|
package-mode = false
|
||||||
|
|
||||||
|
[project.scripts]
|
||||||
|
{{ project_name }} = "ohmyapi.cli:app"
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
# {{ project_name }} settings.py
|
# {{ project_name }} settings.py
|
||||||
PROJECT_NAME = "MyProject"
|
PROJECT_NAME = "MyProject"
|
||||||
DATABASE_URL = "sqlite://db.sqlite3"
|
DATABASE_URL = "sqlite://db.sqlite3"
|
||||||
INSTALLED_APPS = []
|
INSTALLED_APPS = [
|
||||||
|
#"ohmyapi_auth",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,12 @@
|
||||||
from tortoise import fields as field
|
from tortoise.expressions import Q
|
||||||
from .model import Model
|
from tortoise.manager import Manager
|
||||||
|
from tortoise.query_utils import Prefetch
|
||||||
|
from tortoise.queryset import QuerySet
|
||||||
|
from tortoise.signals import (
|
||||||
|
post_delete,
|
||||||
|
post_save,
|
||||||
|
pre_delete,
|
||||||
|
pre_save,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .model import Model, field
|
||||||
|
|
|
||||||
1
src/ohmyapi/db/exceptions.py
Normal file
1
src/ohmyapi/db/exceptions.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from tortoise.exceptions import *
|
||||||
1
src/ohmyapi/db/functions.py
Normal file
1
src/ohmyapi/db/functions.py
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
from tortoise.functions import *
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
import asyncio
|
|
||||||
from pathlib import Path
|
|
||||||
from aerich import Command
|
|
||||||
from ohmyapi.core import runtime
|
|
||||||
|
|
||||||
|
|
||||||
class MigrationManager:
|
|
||||||
def __init__(self, project):
|
|
||||||
self.project = project
|
|
||||||
self._commands = {}
|
|
||||||
# Compute tortoise_config grouped by app module
|
|
||||||
self._tortoise_config = self._build_tortoise_config()
|
|
||||||
|
|
||||||
def _build_tortoise_config(self) -> dict:
|
|
||||||
"""
|
|
||||||
Build Tortoise config from the flat model_registry,
|
|
||||||
grouping models by app module for Aerich compatibility.
|
|
||||||
"""
|
|
||||||
db_url = self.project.settings.DATABASE_URL
|
|
||||||
registry = self.project.model_registry # flat: model_path -> class
|
|
||||||
|
|
||||||
apps_modules = {}
|
|
||||||
for model_path, model_cls in registry.items():
|
|
||||||
if not isinstance(model_cls, type):
|
|
||||||
raise TypeError(f"Registry value must be a class, got {type(model_cls)}: {model_cls}")
|
|
||||||
# Extract app module by removing the model class name
|
|
||||||
# Example: 'ohmyapi.apps.auth.User' -> 'ohmyapi.apps.auth'
|
|
||||||
app_module = ".".join(model_path.split(".")[:-1])
|
|
||||||
apps_modules.setdefault(app_module, []).append(model_cls)
|
|
||||||
|
|
||||||
# Build Tortoise config
|
|
||||||
apps_config = {}
|
|
||||||
for app_module, models in apps_modules.items():
|
|
||||||
modules_set = set(m.__module__ for m in models)
|
|
||||||
apps_config[app_module] = {
|
|
||||||
"models": list(modules_set),
|
|
||||||
"default_connection": "default",
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"connections": {"default": db_url},
|
|
||||||
"apps": apps_config,
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_apps(self):
|
|
||||||
"""Return app modules extracted from the registry"""
|
|
||||||
return list(self._tortoise_config["apps"].keys())
|
|
||||||
|
|
||||||
def get_migration_location(self, app_module: str) -> str:
|
|
||||||
"""Return the path to the app's migrations folder"""
|
|
||||||
try:
|
|
||||||
module = __import__(app_module, fromlist=["migrations"])
|
|
||||||
if not hasattr(module, "__file__") or module.__file__ is None:
|
|
||||||
raise ValueError(f"Cannot determine filesystem path for app '{app_module}'")
|
|
||||||
app_path = Path(module.__file__).parent
|
|
||||||
migrations_path = app_path / "migrations"
|
|
||||||
migrations_path.mkdir(exist_ok=True)
|
|
||||||
return str(migrations_path)
|
|
||||||
except ModuleNotFoundError:
|
|
||||||
raise ValueError(f"App module '{app_module}' cannot be imported")
|
|
||||||
|
|
||||||
async def init_app_command(self, app_module: str) -> Command:
|
|
||||||
"""Initialize Aerich command for a specific app module"""
|
|
||||||
location = self.get_migration_location(app_module)
|
|
||||||
cmd = Command(
|
|
||||||
tortoise_config=self._tortoise_config,
|
|
||||||
app=app_module,
|
|
||||||
location=location,
|
|
||||||
)
|
|
||||||
await cmd.init()
|
|
||||||
self._commands[app_module] = cmd
|
|
||||||
return cmd
|
|
||||||
|
|
||||||
async def makemigrations(self, app_module: str):
|
|
||||||
"""Generate migrations for a specific app"""
|
|
||||||
cmd = self._commands.get(app_module) or await self.init_app_command(app_module)
|
|
||||||
await cmd.migrate()
|
|
||||||
|
|
||||||
async def migrate(self, app_module: str = None):
|
|
||||||
"""Apply migrations. If app_module is None, migrate all apps"""
|
|
||||||
apps_to_migrate = [app_module] if app_module else self.get_apps()
|
|
||||||
for app in apps_to_migrate:
|
|
||||||
cmd = self._commands.get(app) or await self.init_app_command(app)
|
|
||||||
await cmd.upgrade()
|
|
||||||
|
|
||||||
async def show_migrations(self, app_module: str):
|
|
||||||
"""List migrations for an app"""
|
|
||||||
cmd = self._commands.get(app_module) or await self.init_app_command(app_module)
|
|
||||||
await cmd.history()
|
|
||||||
|
|
||||||
|
|
@ -1 +1 @@
|
||||||
from .model import Model, fields
|
from .model import Model, field
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,87 @@
|
||||||
from tortoise import fields
|
from uuid import UUID
|
||||||
from tortoise.models import Model as TortoiseModel
|
|
||||||
|
from pydantic import GetCoreSchemaHandler
|
||||||
|
from pydantic_core import core_schema
|
||||||
|
from tortoise import fields as field
|
||||||
from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
|
from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
|
||||||
|
from tortoise.models import Model as TortoiseModel
|
||||||
|
|
||||||
|
|
||||||
|
def __uuid_schema_monkey_patch(cls, source_type, handler):
|
||||||
|
# Always treat UUID as string schema
|
||||||
|
return core_schema.no_info_after_validator_function(
|
||||||
|
# Accept UUID or str, always return UUID internally
|
||||||
|
lambda v: v if isinstance(v, UUID) else UUID(str(v)),
|
||||||
|
core_schema.union_schema(
|
||||||
|
[
|
||||||
|
core_schema.str_schema(),
|
||||||
|
core_schema.is_instance_schema(UUID),
|
||||||
|
]
|
||||||
|
),
|
||||||
|
# But when serializing, always str()
|
||||||
|
serialization=core_schema.plain_serializer_function_ser_schema(
|
||||||
|
str, when_used="always"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Monkey-patch UUID
|
||||||
|
UUID.__get_pydantic_core_schema__ = classmethod(__uuid_schema_monkey_patch)
|
||||||
|
|
||||||
|
|
||||||
class ModelMeta(type(TortoiseModel)):
|
class ModelMeta(type(TortoiseModel)):
|
||||||
def __new__(cls, name, bases, attrs):
|
def __new__(mcls, name, bases, attrs):
|
||||||
new_cls = super().__new__(cls, name, bases, attrs)
|
meta = attrs.get("Meta", None)
|
||||||
|
if meta is None:
|
||||||
|
class Meta:
|
||||||
|
pass
|
||||||
|
meta = Meta
|
||||||
|
attrs["Meta"] = meta
|
||||||
|
|
||||||
schema_opts = getattr(new_cls, "Schema", None)
|
if not hasattr(meta, "table"):
|
||||||
|
# Use first part of module as app_label
|
||||||
|
app_label = attrs.get("__module__", "").replace("ohmyapi.builtin.", "ohmyapi_").split(".")[0]
|
||||||
|
setattr(meta, "table", f"{app_label}_{name.lower()}")
|
||||||
|
|
||||||
|
# Grab the Schema class for further processing.
|
||||||
|
schema_opts = attrs.get("Schema", None)
|
||||||
|
|
||||||
|
# Let Tortoise's Metaclass do it's thing.
|
||||||
|
new_cls = super().__new__(mcls, name, bases, attrs)
|
||||||
|
|
||||||
class BoundSchema:
|
class BoundSchema:
|
||||||
|
def __call__(self, readonly: bool = False):
|
||||||
|
return self.get(readonly)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def one(self):
|
def model(self):
|
||||||
"""Return a Pydantic model class for 'one' results."""
|
"""Return a Pydantic model class for serializing results."""
|
||||||
include = getattr(schema_opts, "include", None)
|
include = getattr(schema_opts, "include", None)
|
||||||
exclude = getattr(schema_opts, "exclude", None)
|
exclude = getattr(schema_opts, "exclude", None)
|
||||||
return pydantic_model_creator(
|
return pydantic_model_creator(
|
||||||
new_cls,
|
new_cls,
|
||||||
name=f"{new_cls.__name__}SchemaOne",
|
name=f"{new_cls.__name__}Schema",
|
||||||
|
include=include,
|
||||||
|
exclude=exclude,
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def readonly(self):
|
||||||
|
"""Return a Pydantic model class for serializing readonly results."""
|
||||||
|
include = getattr(schema_opts, "include", None)
|
||||||
|
exclude = getattr(schema_opts, "exclude", None)
|
||||||
|
return pydantic_model_creator(
|
||||||
|
new_cls,
|
||||||
|
name=f"{new_cls.__name__}SchemaReadonly",
|
||||||
include=include,
|
include=include,
|
||||||
exclude=exclude,
|
exclude=exclude,
|
||||||
exclude_readonly=True,
|
exclude_readonly=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
@property
|
def get(self, readonly: bool = False):
|
||||||
def many(self):
|
if readonly:
|
||||||
"""Return a Pydantic queryset class for 'many' results."""
|
return self.readonly
|
||||||
include = getattr(schema_opts, "include", None)
|
return self.model
|
||||||
exclude = getattr(schema_opts, "exclude", None)
|
|
||||||
return pydantic_queryset_creator(
|
|
||||||
new_cls,
|
|
||||||
name=f"{new_cls.__name__}SchemaMany",
|
|
||||||
include=include,
|
|
||||||
exclude=exclude,
|
|
||||||
)
|
|
||||||
|
|
||||||
new_cls.Schema = BoundSchema()
|
new_cls.Schema = BoundSchema()
|
||||||
return new_cls
|
return new_cls
|
||||||
|
|
@ -43,4 +91,3 @@ class Model(TortoiseModel, metaclass=ModelMeta):
|
||||||
class Schema:
|
class Schema:
|
||||||
include = None
|
include = None
|
||||||
exclude = None
|
exclude = None
|
||||||
|
|
||||||
|
|
|
||||||
26
src/ohmyapi/middleware/cors.py
Normal file
26
src/ohmyapi/middleware/cors.py
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
from fastapi import FastAPI
|
||||||
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Tuple
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
DEFAULT_ORIGINS = ["http://localhost", "http://localhost:8000"]
|
||||||
|
DEFAULT_CREDENTIALS = False
|
||||||
|
DEFAULT_METHODS = ["*"]
|
||||||
|
DEFAULT_HEADERS = ["*"]
|
||||||
|
|
||||||
|
CORS_CONFIG: Dict[str, Any] = getattr(settings, "MIDDLEWARE_CORS", {})
|
||||||
|
|
||||||
|
if not isinstance(CORS_CONFIG, dict):
|
||||||
|
raise ValueError("MIDDLEWARE_CORS must be of type dict")
|
||||||
|
|
||||||
|
middleware = (
|
||||||
|
CORSMiddleware,
|
||||||
|
{
|
||||||
|
"allow_origins": CORS_CONFIG.get("ALLOW_ORIGINS", DEFAULT_ORIGINS),
|
||||||
|
"allow_credentials": CORS_CONFIG.get("ALLOW_CREDENTIALS", DEFAULT_CREDENTIALS),
|
||||||
|
"allow_methods": CORS_CONFIG.get("ALLOW_METHODS", DEFAULT_METHODS),
|
||||||
|
"allow_headers": CORS_CONFIG.get("ALLOW_HEADERS", DEFAULT_HEADERS),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
@ -1,2 +1,3 @@
|
||||||
from fastapi import APIRouter, Depends
|
from http import HTTPStatus
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue