Compare commits

..

73 commits
0.2.2 ... main

Author SHA1 Message Date
Brian Wiborg
b5691f3133
🔖 0.6.2 2025-11-05 21:37:34 +01:00
Brian Wiborg
5f80a7a86f
🐛 Fix model-free apps and middleware installer 2025-11-05 21:29:25 +01:00
Brian Wiborg
b588ebcf8a
🚑️ Remove roles claim 2025-10-28 14:45:18 +01:00
Brian Wiborg
a9b88d87d6
🔖 0.6.0 2025-10-28 14:40:29 +01:00
Brian Wiborg
7163fe778e
♻️ Refactor ohmyapi_auth
- remove Group and UserGroups
  (should be handled by dedicated app, if even)
- enforce User.Schema() include-fields
2025-10-28 14:39:42 +01:00
Brian Wiborg
458ffc6b2c
🔖 0.5.6 2025-10-27 11:13:02 +01:00
Brian Wiborg
22ca522615
🐛 Catch invalid user refresh 2025-10-27 11:03:12 +01:00
Brian Wiborg
8c2cf01f40
🔖 0.5.5 2025-10-27 10:47:05 +01:00
Brian Wiborg
9d2e284da3
🐛 Strict proxy-table field naming
This worked in SQlite3, but threw when using PostgreSQL.
2025-10-27 10:45:19 +01:00
Brian Wiborg
ed30291a4c
🔖 0.5.4 2025-10-26 21:49:13 +01:00
Brian Wiborg
31f4da773c
⬆️ Upgrade deps 2025-10-26 21:48:48 +01:00
Brian Wiborg
715b7a030a
🐛 Createsuperuser in single asyncio task 2025-10-26 21:47:03 +01:00
Brian Wiborg
4a5bafd889
🔖 0.5.3 2025-10-25 11:18:07 +02:00
Brian Wiborg
b50cbc4341
🐛 Fix /auth/refresh 2025-10-25 11:17:05 +02:00
Brian Wiborg
10681cc15b
🚚 Move hmac_hash() to ohmyapi_auth.utils 2025-10-24 20:54:16 +02:00
Brian Wiborg
58f1387aaf
🔖 0.5.2 2025-10-22 11:26:50 +02:00
Brian Wiborg
6b87bfeefb
🐛 email_hash is not optional 2025-10-22 11:25:40 +02:00
Brian Wiborg
812e89ede9
🔖 0.5.1 2025-10-11 13:31:28 +02:00
Brian Wiborg
e25c9d1715
🩹 Define explicit user_groups proxy-table 2025-10-11 13:30:46 +02:00
Brian Wiborg
d494396728
✏️ Fix typo 2025-10-11 13:12:48 +02:00
Brian Wiborg
2e1ec5d780
🔖 0.5.0 2025-10-11 13:06:34 +02:00
Brian Wiborg
ce47e3f60e
🎨 Auto-prefix table_names with "{app_label}_"
There is no support for auto-prefixing implicit proxy-tables in
Tortoise. If you need to prefix a proxy-table, explicitly define the
Model for the proxy table. Being an explicit model, it will then be
auto-prefixed.
2025-10-11 13:03:31 +02:00
Brian Wiborg
acd4844a25
🎨 Export tortoise.functions as ohmyapi.db.functions 2025-10-11 04:31:30 +02:00
Brian Wiborg
74f625ab1d
🔖 0.4.7 2025-10-11 02:22:15 +02:00
Brian Wiborg
a45f03b92f
🐛 Fix maybe_authenticated 2025-10-11 02:20:59 +02:00
Brian Wiborg
de043ddd97
🐛 Fix authenticate method 2025-10-11 02:20:45 +02:00
Brian Wiborg
80163ce994
🐛 Make settings module available, first 2025-10-11 02:06:42 +02:00
Brian Wiborg
66176e9af7
🔖 0.4.6 2025-10-11 01:21:03 +02:00
Brian Wiborg
cf106e8855
🐛 maybe_authenticated with export 2025-10-11 01:20:32 +02:00
Brian Wiborg
d7f7db338f
🔖 0.4.5 2025-10-11 01:08:50 +02:00
Brian Wiborg
643a6b2eb7
🔒️ Add optionally_authenticated permission 2025-10-11 01:07:55 +02:00
Brian Wiborg
1c42b44d41
🔖 0.4.4 2025-10-10 15:49:57 +02:00
Brian Wiborg
2239480dc0
🎨 Also export tortoise.query_utils.Prefetch 2025-10-10 15:49:14 +02:00
Brian Wiborg
3e15aa7722
🐛 Support apps without any models 2025-10-10 15:48:40 +02:00
Brian Wiborg
e1f5ce589c
🔖 0.4.3 2025-10-08 13:56:23 +02:00
Brian Wiborg
4ec2f87ce2
🐛 Fix ohmyapi.core.runtime.App.models 2025-10-08 13:55:57 +02:00
Brian Wiborg
e3abc642ed
🔖 0.4.2 2025-10-08 13:40:28 +02:00
Brian Wiborg
b1222c64d6
🐛 Fix ModuleNotFoundError typo 2025-10-08 13:39:11 +02:00
Brian Wiborg
1ef64c2b17
🔖 0.4.1 2025-10-08 13:20:32 +02:00
Brian Wiborg
49c24b7f0c
🐛 Fix createsuperuser 2025-10-08 13:19:47 +02:00
Brian Wiborg
4c11297d65
🔖 0.4.0 2025-10-08 13:10:30 +02:00
Brian Wiborg
c23576b393
🩹 Remove ohmyapi from template deps 2025-10-08 13:09:16 +02:00
Brian Wiborg
0deb5706f8
🎨 Single line initialization is more readable 2025-10-08 13:07:31 +02:00
Brian Wiborg
f579972466
🎨 Allow overriding base prefix 2025-10-08 13:06:42 +02:00
Brian Wiborg
cb9acd52d0
📝 Small improvements 2025-10-08 12:54:42 +02:00
Brian Wiborg
67d89a94f4
Integrate logging 2025-10-08 12:45:27 +02:00
Brian Wiborg
e848601f79
🐛 Use set_email in createsuperuser 2025-10-08 12:41:05 +02:00
Brian Wiborg
6120c151f1
🎨 Add Postgress service as comment 2025-10-06 21:31:10 +02:00
Brian Wiborg
3e029e1fb7
🔥 Remove poetry lock 2025-10-06 21:30:50 +02:00
Brian Wiborg
9ac95af005
🎨 Add option to pull in asyncpg 2025-10-06 21:29:52 +02:00
Brian Wiborg
33a9c94042
♻️ Refactor ohmyapi_auth
- remove middlewares (should come from user)
- save symmetrically hased e-mail instead of raw
2025-10-06 21:27:16 +02:00
Brian Wiborg
cc3872cf74
Add support for middleware in apps 2025-10-02 21:50:42 +02:00
Brian Wiborg
5c632cbe8f
🎨 Add Q to exports 2025-10-02 17:15:39 +02:00
Brian Wiborg
ad8986abb1
🔖 0.3.2 2025-10-02 15:11:19 +02:00
Brian Wiborg
bcdd23652f
🐛 Revoke support for table_name prefixes
Unfortunately, Aerich seems a bit awkward in respecting
Model.Meta.table. Also proxy-tables can not be prefixed at all.

If there is no concise way to prefix all tables of an app, we shouldn't
prefix it at all and leave the collision-problem for the user to keep an
eye on.
2025-10-02 15:09:02 +02:00
Brian Wiborg
856ea12f52
🔖 0.3.1 2025-10-02 14:47:15 +02:00
Brian Wiborg
bb8884f419
🐛 Remember to alias builtin apps before inferring 2025-10-02 14:46:32 +02:00
Brian Wiborg
63d2c31763
🔖 0.3.0 2025-10-02 14:32:47 +02:00
Brian Wiborg
64e98f9f0a
🏗️ Automatically prefix table_names with app_label 2025-10-02 14:30:53 +02:00
Brian Wiborg
c411a9795c
🔖 0.2.8 2025-10-02 05:48:48 +02:00
Brian Wiborg
e2f968bac4
👷 Add dockerize command 2025-10-02 05:47:48 +02:00
Brian Wiborg
e9d0fb5b80
🔖 0.2.6 2025-10-02 04:51:41 +02:00
Brian Wiborg
f74b20a19f
♻️ Provide project.configure_app 2025-10-02 04:50:55 +02:00
Brian Wiborg
6089f950b6
👷 Dynamic versioning 2025-10-02 04:49:59 +02:00
Brian Wiborg
e894c8f100
🔖 0.2.5 2025-10-02 03:09:23 +02:00
Brian Wiborg
26a072714d
🩹 Pass APIRouters directly 2025-10-02 03:08:24 +02:00
Brian Wiborg
795dab71f0
⬆️ Upgrade all deps 2025-10-02 03:02:44 +02:00
Brian Wiborg
af110cec9d
🩹 Fix FastAPI app initialization 2025-10-02 03:02:00 +02:00
Brian Wiborg
ee4bd2760c
🔖 0.2.4 2025-10-02 02:46:23 +02:00
Brian Wiborg
28f76fc4f4
🩹 Fix typo 2025-10-02 02:45:53 +02:00
Brian Wiborg
63a4f4f948
🩹 This app's models only 2025-10-02 02:45:14 +02:00
Brian Wiborg
37d807eb65
🔖 0.2.3 2025-10-02 02:06:36 +02:00
Brian Wiborg
f0e5c8c30e
📝 Add builtin section; add ohmyapi_auth docs 2025-10-02 02:05:25 +02:00
23 changed files with 668 additions and 177 deletions

View file

@ -1,8 +1,9 @@
# OhMyAPI # OhMyAPI
OhMyAPI is a web-application scaffolding framework and management layer built around `FastAPI`, `TortoiseORM` and `Aerich` migrations. 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.
> *Think: Django RestFramework, but less clunky and 100% async.* > *Think: *"Django RestFramework"*, but less clunky and instead 100% async.*
It is ***blazingly fast***, extremely ***fun to use*** and comes with ***batteries included***! It is ***blazingly fast***, extremely ***fun to use*** and comes with ***batteries included***!
@ -12,7 +13,7 @@ It is ***blazingly fast***, extremely ***fun to use*** and comes with ***batteri
- Django-like per-app migrations (makemigrations & migrate) via Aerich - Django-like per-app migrations (makemigrations & migrate) via Aerich
- Django-like CLI tooling (startproject, startapp, shell, serve, etc) - Django-like CLI tooling (startproject, startapp, shell, serve, etc)
- Customizable pydantic model serializer built-in - Customizable pydantic model serializer built-in
- Various optional built-in apps you can hook into your project - Various optional built-in apps you can hook into your project (i.e. authentication and more)
- Highly configurable and customizable - Highly configurable and customizable
- 100% async - 100% async
@ -31,10 +32,10 @@ pipx install ohmyapi
## Docs ## Docs
See: `docs/` See `docs/` or:
- [Projects](docs/projects.md) ```
- [Apps](docs/apps.md) poetry run mkdocs serve
- [Models](docs/models.md) ```
- [Migrations](docs/migrations.md)
- [Routes](docs/routes.md) Go to: [http://localhost:8000/](http://localhost:8000/)

8
docs/README.md Normal file
View 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)

View file

@ -1,7 +1,7 @@
# Apps # Apps
Apps are a way to group database models and API routes that contextually belong together. Apps are a way to group database models and API routes that contextually belong together.
For example, OhMyAPI comes bundled with an `auth` app that carries a `User` and `Group` model and provides API endpoints for JWT authentication. 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. Apps help organizing projects by isolating individual components (or "features") from one another.

45
docs/auth.md Normal file
View 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)):
...
```

View file

@ -7,3 +7,5 @@ nav:
- Models: models.md - Models: models.md
- Migrations: migrations.md - Migrations: migrations.md
- Routes: routes.md - Routes: routes.md
- Builtin:
- Auth / Permissions: auth.md

289
poetry.lock generated
View file

@ -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"
@ -62,7 +62,7 @@ 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.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"}, {file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"},
{file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"}, {file = "anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4"},
@ -216,7 +216,7 @@ 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"},
@ -543,19 +543,66 @@ 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"},
@ -564,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", "dev"] 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]
@ -592,10 +651,10 @@ 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"
@ -627,14 +686,14 @@ files = [
[[package]] [[package]]
name = "isort" name = "isort"
version = "6.0.1" version = "6.1.0"
description = "A Python utility / library to sort Python imports." description = "A Python utility / library to sort Python imports."
optional = false optional = false
python-versions = ">=3.9.0" python-versions = ">=3.9.0"
groups = ["dev"] groups = ["dev"]
files = [ files = [
{file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"}, {file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"},
{file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"}, {file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"},
] ]
[package.extras] [package.extras]
@ -721,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", "dev"] 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]]
@ -996,6 +1083,22 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-a
test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"]
type = ["mypy (>=1.14.1)"] 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"
@ -1232,6 +1335,28 @@ 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]] [[package]]
name = "python-dateutil" name = "python-dateutil"
version = "2.9.0.post0" version = "2.9.0.post0"
@ -1460,7 +1585,7 @@ 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"},
@ -1576,18 +1701,18 @@ 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.11\""} 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]
@ -1691,4 +1816,4 @@ auth = ["argon2-cffi", "crypto", "passlib", "pyjwt", "python-multipart"]
[metadata] [metadata]
lock-version = "2.1" lock-version = "2.1"
python-versions = ">=3.11" python-versions = ">=3.11"
content-hash = "3d301460081dada359d425d69feefc63c1e5135aa64b6f000f554bfc1231febd" content-hash = "cc1604995d3b73ee302e63731dd300ea17c4d95d0cfc6c386626dd9a9f60e8a7"

View file

@ -1,13 +1,12 @@
[project] [project]
name = "ohmyapi" name = "ohmyapi"
version = "0.2.2" version = "0.6.2"
description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations" description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations"
license = "MIT" license = "MIT"
keywords = ["fastapi", "tortoise", "orm", "pydantic", "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"}
] ]
repository = "https://code.c-base.org/baccenfutter/ohmyapi"
readme = "README.md" readme = "README.md"
requires-python = ">=3.11" requires-python = ">=3.11"
@ -31,6 +30,8 @@ ipython = ">=9.5.0,<10.0.0"
black = "^25.9.0" black = "^25.9.0"
isort = "^6.0.1" isort = "^6.0.1"
mkdocs = "^1.6.1" mkdocs = "^1.6.1"
pytest = "^8.4.2"
httpx = "^0.28.1"
[project.optional-dependencies] [project.optional-dependencies]
auth = ["passlib", "pyjwt", "crypto", "argon2-cffi", "python-multipart"] auth = ["passlib", "pyjwt", "crypto", "argon2-cffi", "python-multipart"]
@ -41,6 +42,22 @@ packages = [ { include = "ohmyapi", from = "src" } ]
[project.scripts] [project.scripts]
ohmyapi = "ohmyapi.cli:app" ohmyapi = "ohmyapi.cli:app"
[project.urls]
repository = "https://code.c-base.org/baccenfutter/ohmyapi"
[build-system]
requires = [
"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] [tool.black]
line-length = 88 line-length = 88
target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] target-version = ['py39', 'py310', 'py311', 'py312', 'py313']

View file

@ -1 +1 @@
from . import db __VERSION__ = "0.6.2"

View file

@ -1,39 +1,52 @@
from functools import wraps from ohmyapi.db import Model, field, Q
from typing import List, Optional
from uuid import UUID
from passlib.context import CryptContext
from tortoise.contrib.pydantic import pydantic_queryset_creator
from ohmyapi.db import Model, field, pre_delete, pre_save
from ohmyapi.router import HTTPException from ohmyapi.router import HTTPException
from .utils import hmac_hash
from datetime import datetime
from passlib.context import CryptContext
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: UUID = field.data.UUIDField(pk=True)
name: str = field.CharField(max_length=42, index=True)
class User(Model): class User(Model):
id: UUID = field.data.UUIDField(pk=True) id: UUID = field.data.UUIDField(pk=True)
email: str = field.CharField(max_length=255, unique=True, index=True)
username: str = field.CharField(max_length=150, unique=True) username: str = field.CharField(max_length=150, unique=True)
email_hash: str = field.CharField(max_length=255, unique=True, index=True)
password_hash: str = field.CharField(max_length=128) password_hash: str = field.CharField(max_length=128)
is_admin: bool = field.BooleanField(default=False) is_admin: bool = field.BooleanField(default=False)
is_staff: bool = field.BooleanField(default=False) is_staff: bool = field.BooleanField(default=False)
groups: field.ManyToManyRelation[Group] = field.ManyToManyField( created_at: datetime = field.DatetimeField(auto_now_add=True)
"ohmyapi_auth.Group", related_name="users", through="usergroups" 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)

View file

@ -1,6 +1,7 @@
from .routes import ( from .routes import (
get_current_user, get_current_user,
get_token, get_token,
maybe_authenticated,
require_admin, require_admin,
require_authenticated, require_authenticated,
require_group, require_group,

View file

@ -1,14 +1,17 @@
import time import time
from enum import Enum from enum import Enum
from typing import Any, Dict, List from typing import Any, Dict, List, Optional
from fastapi import APIRouter, Body, Depends, Header, HTTPException, Request, status
from fastapi.security import OAuth2, OAuth2PasswordBearer, OAuth2PasswordRequestForm
from fastapi.security.utils import get_authorization_scheme_param
from pydantic import BaseModel
from tortoise.exceptions import DoesNotExist
from ohmyapi.builtin.auth.models import User
import jwt import jwt
import settings import settings
from fastapi import APIRouter, Body, Depends, Header, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from pydantic import BaseModel
from ohmyapi.builtin.auth.models import Group, User
# Router # Router
router = APIRouter(prefix="/auth", tags=["Auth"]) router = APIRouter(prefix="/auth", tags=["Auth"])
@ -23,12 +26,25 @@ REFRESH_TOKEN_EXPIRE_SECONDS = getattr(
settings, "JWT_REFRESH_TOKEN_EXPIRE_SECONDS", 7 * 24 * 60 * 60 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")
class ClaimsUser(BaseModel): class ClaimsUser(BaseModel):
username: str username: str
email: str
is_admin: bool is_admin: bool
is_staff: bool is_staff: bool
@ -37,7 +53,6 @@ class Claims(BaseModel):
type: str type: str
sub: str sub: str
user: ClaimsUser user: ClaimsUser
roles: List[str]
exp: str exp: str
@ -64,17 +79,15 @@ class TokenType(str, Enum):
refresh = "refresh" refresh = "refresh"
def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claims: def claims(token_type: TokenType, user: User = []) -> Claims:
return Claims( return Claims(
type=token_type, type=token_type,
sub=str(user.id), sub=str(user.id),
user=ClaimsUser( user=ClaimsUser(
username=user.username, username=user.username,
email=user.email,
is_admin=user.is_admin, is_admin=user.is_admin,
is_staff=user.is_staff, is_staff=user.is_staff,
), ),
roles=[g.name for g in groups],
exp="", exp="",
) )
@ -124,6 +137,12 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
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:
@ -180,17 +199,25 @@ async def login(form_data: LoginRequest = Body(...)):
) )
class TokenRefresh(BaseModel):
refresh_token: str
@router.post("/refresh", response_model=AccessToken) @router.post("/refresh", response_model=AccessToken)
async def refresh_token(refresh_token: str): 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( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token" status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
) )
user_id = payload.get("sub") user_id = payload.get("sub")
user = await User.filter(id=user_id).first() try:
user = await User.get(id=user_id)
except DoesNotExist:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED)
if not user: if not user:
raise HTTPException( raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"
@ -199,7 +226,7 @@ async def refresh_token(refresh_token: str):
new_access = create_token( new_access = create_token(
claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS
) )
return AccessToken(token_type="bearer", access_token=access_token) return AccessToken(token_type="bearer", access_token=new_access)
@router.get("/introspect", response_model=Dict[str, Any]) @router.get("/introspect", response_model=Dict[str, Any])

View 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")

View file

@ -9,6 +9,9 @@ import typer
import uvicorn import uvicorn
from ohmyapi.core import runtime, scaffolding from ohmyapi.core import runtime, scaffolding
from ohmyapi.core.logging import setup_logging
logger = setup_logging()
app = typer.Typer( app = typer.Typer(
help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM." help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM."
@ -19,12 +22,24 @@ app = typer.Typer(
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()
@ -34,12 +49,13 @@ 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."""
project_path = Path(root).resolve() project_path = Path(root).resolve()
project = runtime.Project(project_path) project = runtime.Project(project_path)
@ -67,7 +83,7 @@ def shell(root: str = "."):
asyncio.set_event_loop(loop) asyncio.set_event_loop(loop)
loop.run_until_complete(init_and_cleanup()) loop.run_until_complete(init_and_cleanup())
# Prepare shell vars that are to be directly available # Prepare shell vars that are to be immediately available
shell_vars = {"p": project} shell_vars = {"p": project}
try: try:
@ -99,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()
@ -113,6 +131,7 @@ 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()
@ -142,9 +161,17 @@ def createsuperuser(root: str = "."):
if password1 != password2: if password1 != password2:
print("Passwords didn't match!") print("Passwords didn't match!")
user = ohmyapi_auth.models.User( user = ohmyapi_auth.models.User(
email=email, username=username, is_staff=True, is_admin=True username=username, is_staff=True, is_admin=True
) )
user.set_email(email)
user.set_password(password1) user.set_password(password1)
asyncio.run(project.init_orm())
asyncio.run(user.save()) async def _run():
asyncio.run(project.close_orm()) await project.init_orm()
user = ohmyapi_auth.models.User(username=username, is_staff=True, is_admin=True)
user.set_email(email)
user.set_password(password1)
await user.save()
await project.close_orm()
asyncio.run(_run())

View 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

View file

@ -7,7 +7,7 @@ import sys
from http import HTTPStatus from http import HTTPStatus
from pathlib import Path from pathlib import Path
from types import ModuleType from types import ModuleType
from typing import Any, Dict, Generator, List, Optional, Type 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
@ -15,8 +15,11 @@ from aerich.exceptions import NotInitedError
from fastapi import APIRouter, FastAPI from fastapi import APIRouter, FastAPI
from tortoise import Tortoise from tortoise import Tortoise
from ohmyapi.core.logging import setup_logging
from ohmyapi.db.model import Model from ohmyapi.db.model import Model
logger = setup_logging()
class Project: class Project:
""" """
@ -29,6 +32,7 @@ class Project:
""" """
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"
@ -36,6 +40,14 @@ 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))
# 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
# Alias builtin apps as ohmyapi_<name>. # Alias builtin apps as ohmyapi_<name>.
# We need this, because Tortoise app-names may not include dots `.`. # 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")
@ -53,14 +65,6 @@ class Project:
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)
@ -72,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()
@ -109,7 +129,7 @@ class Project:
} }
for app_name, app in self._apps.items(): for app_name, app in self._apps.items():
modules = list(app.models.keys()) modules = list(app._models.keys())
if modules: if modules:
config["apps"][app_name] = { config["apps"][app_name] = {
"models": modules, "models": modules,
@ -134,7 +154,10 @@ class Project:
tortoise_cfg = 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. # Prevent leaking other app's models to Aerich.
tortoise_cfg["apps"] = {app_label: tortoise_cfg["apps"][app_label]} 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
tortoise_cfg["apps"][app_label]["models"].append("aerich.models") tortoise_cfg["apps"][app_label]["models"].append("aerich.models")
@ -217,11 +240,16 @@ class App:
# Reference to this app's routes modules. # Reference to this app's routes modules.
self._routers: Dict[str, ModuleType] = {} 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. # Import the app, so its __init__.py runs.
mod: ModuleType = importlib.import_module(name) mod: ModuleType = importlib.import_module(name)
logger.debug(f"Loading app: {self.name}")
self.__load_models(f"{self.name}.models") self.__load_models(f"{self.name}.models")
self.__load_routes(f"{self.name}.routes") self.__load_routes(f"{self.name}.routes")
self.__load_middlewares(f"{self.name}.middlewares")
def __repr__(self): def __repr__(self):
return json.dumps(self.dict(), indent=2) return json.dumps(self.dict(), indent=2)
@ -239,7 +267,6 @@ class App:
try: try:
importlib.import_module(mod_name) importlib.import_module(mod_name)
except ModuleNotFoundError: except ModuleNotFoundError:
print(f"no models detected: {mod_name}")
return return
# Acoid duplicates. # Acoid duplicates.
@ -257,7 +284,11 @@ class App:
and issubclass(value, Model) and issubclass(value, Model)
and not name == Model.__name__ and not name == Model.__name__
): ):
self._models[mod_name] = self._models.get(mod_name, []) + [value] # 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 it's a package, recurse into submodules
if hasattr(mod, "__path__"): if hasattr(mod, "__path__"):
@ -278,8 +309,7 @@ class App:
# An app may come without any routes. # An app may come without any routes.
try: try:
importlib.import_module(mod_name) importlib.import_module(mod_name)
except ModuleNotFound: except ModuleNotFoundError:
print(f"no routes detected: {mod_name}")
return return
# Avoid duplicates. # Avoid duplicates.
@ -294,18 +324,29 @@ class App:
for name, value in vars(mod).copy().items(): for name, value in vars(mod).copy().items():
if isinstance(value, APIRouter) and not name == APIRouter.__name__: if isinstance(value, APIRouter) and not name == APIRouter.__name__:
self._routers[mod_name] = self._routers.get(mod_name, []) + [value] 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 it's a package, recurse into submodules
if hasattr(mod, "__path__"): if hasattr(mod, "__path__"):
for _, subname, _ in pkgutil.iter_modules( for _, subname, _ in pkgutil.iter_modules(
mod.__path__, mod.__name__ + "." mod.__path__, mod.__name__ + "."
): ):
submod = importlib.import_module(subname) walk(subname)
walk(submod)
# Walk the walk. # Walk the walk.
walk(mod_name) 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): def __serialize_route(self, route):
""" """
Convert APIRoute to JSON-serializable dict. Convert APIRoute to JSON-serializable dict.
@ -319,6 +360,12 @@ class App:
def __serialize_router(self): def __serialize_router(self):
return [self.__serialize_route(route) for route in self.routes] 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 @property
def models(self) -> List[ModuleType]: def models(self) -> List[ModuleType]:
""" """
@ -328,28 +375,44 @@ class App:
for module in self._models: for module in self._models:
for model in self._models[module]: for model in self._models[module]:
out.append(model) out.append(model)
return { return out
module: 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 @property
def routes(self): def routes(self):
""" """
Return an APIRouter with all loaded routes. Return an APIRouter with all loaded routes.
""" """
router = APIRouter() out = []
for routes_mod in self._routers: for r in self.routers:
for r in self._routers[routes_mod]: out.extend(r.routes)
router.include_router(r) return out
return router.routes
@property
def middlewares(self):
"""Returns the list of this app's middlewares."""
return self._middlewares
def dict(self) -> Dict[str, Any]: def dict(self) -> Dict[str, Any]:
""" """
Convenience method for serializing the runtime data. 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 { return {
"models": [ "models": models,
f"{self.name}.{m.__name__}" for m in self.models[f"{self.name}.models"] "middlewares": self.__serialize_middleware(),
],
"routes": self.__serialize_router(), "routes": self.__serialize_router(),
} }

View file

@ -2,10 +2,16 @@ 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."""
@ -53,13 +59,29 @@ def render_template_dir(
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()
target_dir.mkdir(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):
@ -72,5 +94,3 @@ def startapp(name: str, project: str):
{"project_name": target_dir.resolve().name, "app_name": name}, {"project_name": target_dir.resolve().name, "app_name": name},
subdir_name=name, subdir_name=name,
) )
print(f"✅ App '{name}' created in project '{target_dir}' successfully.")
print(f"🔧 Remember to add '{name}' to your INSTALLED_APPS!")

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

View 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

View file

@ -10,7 +10,7 @@ readme = "README.md"
license = { text = "MIT" } license = { text = "MIT" }
dependencies = [ dependencies = [
"ohmyapi (>=0.1.0,<0.2.0)" # "asyncpg"
] ]
[tool.poetry.group.dev.dependencies] [tool.poetry.group.dev.dependencies]

View file

@ -1,4 +1,6 @@
from tortoise.expressions import Q
from tortoise.manager import Manager from tortoise.manager import Manager
from tortoise.query_utils import Prefetch
from tortoise.queryset import QuerySet from tortoise.queryset import QuerySet
from tortoise.signals import ( from tortoise.signals import (
post_delete, post_delete,

View file

@ -0,0 +1 @@
from tortoise.functions import *

View file

@ -31,6 +31,18 @@ UUID.__get_pydantic_core_schema__ = classmethod(__uuid_schema_monkey_patch)
class ModelMeta(type(TortoiseModel)): class ModelMeta(type(TortoiseModel)):
def __new__(mcls, name, bases, attrs): def __new__(mcls, name, bases, attrs):
meta = attrs.get("Meta", None)
if meta is None:
class Meta:
pass
meta = Meta
attrs["Meta"] = meta
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. # Grab the Schema class for further processing.
schema_opts = attrs.get("Schema", None) schema_opts = attrs.get("Schema", None)

View 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),
}
)