Compare commits
No commits in common. "main" and "0.2.2" have entirely different histories.
23 changed files with 177 additions and 668 deletions
19
README.md
19
README.md
|
|
@ -1,9 +1,8 @@
|
|||
# OhMyAPI
|
||||
|
||||
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 web-application scaffolding framework and management layer built around `FastAPI`, `TortoiseORM` and `Aerich` migrations.
|
||||
|
||||
> *Think: *"Django RestFramework"*, but less clunky and instead 100% async.*
|
||||
> *Think: Django RestFramework, but less clunky and 100% async.*
|
||||
|
||||
It is ***blazingly fast***, extremely ***fun to use*** and comes with ***batteries included***!
|
||||
|
||||
|
|
@ -13,7 +12,7 @@ It is ***blazingly fast***, extremely ***fun to use*** and comes with ***batteri
|
|||
- 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 (i.e. authentication and more)
|
||||
- Various optional built-in apps you can hook into your project
|
||||
- Highly configurable and customizable
|
||||
- 100% async
|
||||
|
||||
|
|
@ -32,10 +31,10 @@ pipx install ohmyapi
|
|||
|
||||
## Docs
|
||||
|
||||
See `docs/` or:
|
||||
See: `docs/`
|
||||
|
||||
```
|
||||
poetry run mkdocs serve
|
||||
```
|
||||
|
||||
Go to: [http://localhost:8000/](http://localhost:8000/)
|
||||
- [Projects](docs/projects.md)
|
||||
- [Apps](docs/apps.md)
|
||||
- [Models](docs/models.md)
|
||||
- [Migrations](docs/migrations.md)
|
||||
- [Routes](docs/routes.md)
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
# OhMyAPI Docs
|
||||
|
||||
- [Projects](projects.md)
|
||||
- [Apps](apps.md)
|
||||
- [Models](models.md)
|
||||
- [Migrations](migrations.md)
|
||||
- [Routes](routes.md)
|
||||
- [Auth/Permissions](auth.md)
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
# 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.
|
||||
For example, OhMyAPI comes bundled with an `auth` app that carries a `User` and `Group` model and provides API endpoints for JWT authentication.
|
||||
|
||||
Apps help organizing projects by isolating individual components (or "features") from one another.
|
||||
|
||||
|
|
|
|||
45
docs/auth.md
45
docs/auth.md
|
|
@ -1,45 +0,0 @@
|
|||
# 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)):
|
||||
...
|
||||
```
|
||||
|
|
@ -7,5 +7,3 @@ nav:
|
|||
- Models: models.md
|
||||
- Migrations: migrations.md
|
||||
- Routes: routes.md
|
||||
- Builtin:
|
||||
- Auth / Permissions: auth.md
|
||||
|
|
|
|||
289
poetry.lock
generated
289
poetry.lock
generated
|
|
@ -1,4 +1,4 @@
|
|||
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.2.0 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "aerich"
|
||||
|
|
@ -62,7 +62,7 @@ version = "4.11.0"
|
|||
description = "High-level concurrency and networking framework on top of asyncio or Trio"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc"},
|
||||
{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."
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5"},
|
||||
{file = "certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407"},
|
||||
|
|
@ -543,66 +543,19 @@ version = "0.16.0"
|
|||
description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main", "dev"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"},
|
||||
{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]]
|
||||
name = "idna"
|
||||
version = "3.10"
|
||||
description = "Internationalized Domain Names in Applications (IDNA)"
|
||||
optional = false
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main", "dev"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"},
|
||||
{file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"},
|
||||
|
|
@ -611,28 +564,16 @@ files = [
|
|||
[package.extras]
|
||||
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]]
|
||||
name = "ipython"
|
||||
version = "9.6.0"
|
||||
version = "9.5.0"
|
||||
description = "IPython: Productive Interactive Computing"
|
||||
optional = false
|
||||
python-versions = ">=3.11"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "ipython-9.6.0-py3-none-any.whl", hash = "sha256:5f77efafc886d2f023442479b8149e7d86547ad0a979e9da9f045d252f648196"},
|
||||
{file = "ipython-9.6.0.tar.gz", hash = "sha256:5603d6d5d356378be5043e69441a072b50a5b33b4503428c77b04cb8ce7bc731"},
|
||||
{file = "ipython-9.5.0-py3-none-any.whl", hash = "sha256:88369ffa1d5817d609120daa523a6da06d02518e582347c29f8451732a9c5e72"},
|
||||
{file = "ipython-9.5.0.tar.gz", hash = "sha256:129c44b941fe6d9b82d36fc7a7c18127ddb1d6f02f78f867f402e2e3adde3113"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -651,10 +592,10 @@ typing_extensions = {version = ">=4.6", markers = "python_version < \"3.12\""}
|
|||
[package.extras]
|
||||
all = ["ipython[doc,matplotlib,test,test-extra]"]
|
||||
black = ["black"]
|
||||
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 (>3.7)"]
|
||||
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"]
|
||||
matplotlib = ["matplotlib"]
|
||||
test = ["packaging", "pytest", "pytest-asyncio", "testpath"]
|
||||
test-extra = ["curio", "ipykernel", "ipython[matplotlib]", "ipython[test]", "jupyter_ai", "nbclient", "nbformat", "numpy (>=1.25)", "pandas (>2.0)", "trio"]
|
||||
test-extra = ["curio", "ipykernel", "ipython[test]", "jupyter_ai", "matplotlib (!=3.2.0)", "nbclient", "nbformat", "numpy (>=1.23)", "pandas", "trio"]
|
||||
|
||||
[[package]]
|
||||
name = "ipython-pygments-lexers"
|
||||
|
|
@ -686,14 +627,14 @@ files = [
|
|||
|
||||
[[package]]
|
||||
name = "isort"
|
||||
version = "6.1.0"
|
||||
version = "6.0.1"
|
||||
description = "A Python utility / library to sort Python imports."
|
||||
optional = false
|
||||
python-versions = ">=3.9.0"
|
||||
groups = ["dev"]
|
||||
files = [
|
||||
{file = "isort-6.1.0-py3-none-any.whl", hash = "sha256:58d8927ecce74e5087aef019f778d4081a3b6c98f15a80ba35782ca8a2097784"},
|
||||
{file = "isort-6.1.0.tar.gz", hash = "sha256:9b8f96a14cfee0677e78e941ff62f03769a06d412aabb9e2a90487b3b7e8d481"},
|
||||
{file = "isort-6.0.1-py3-none-any.whl", hash = "sha256:2dc5d7f65c9678d94c88dfc29161a320eec67328bc97aad576874cb4be1e9615"},
|
||||
{file = "isort-6.0.1.tar.gz", hash = "sha256:1cb5df28dfbc742e490c5e41bad6da41b805b0a8be7bc93cd0fb2a8a890ac450"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
|
|
@ -780,101 +721,73 @@ testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"]
|
|||
|
||||
[[package]]
|
||||
name = "markupsafe"
|
||||
version = "3.0.3"
|
||||
version = "3.0.2"
|
||||
description = "Safely add untrusted strings to HTML/XML markup."
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main", "dev"]
|
||||
files = [
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"},
|
||||
{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.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"},
|
||||
{file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"},
|
||||
{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.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"},
|
||||
{file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"},
|
||||
{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.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"},
|
||||
{file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"},
|
||||
{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.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"},
|
||||
{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.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"},
|
||||
{file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"},
|
||||
{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.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"},
|
||||
{file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"},
|
||||
{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"},
|
||||
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"},
|
||||
{file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"},
|
||||
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"},
|
||||
{file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"},
|
||||
{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.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"},
|
||||
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"},
|
||||
{file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"},
|
||||
{file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"},
|
||||
{file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"},
|
||||
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"},
|
||||
{file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"},
|
||||
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"},
|
||||
{file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"},
|
||||
{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.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"},
|
||||
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"},
|
||||
{file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"},
|
||||
{file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"},
|
||||
{file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"},
|
||||
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"},
|
||||
{file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"},
|
||||
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"},
|
||||
{file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"},
|
||||
{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.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"},
|
||||
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"},
|
||||
{file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"},
|
||||
{file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"},
|
||||
{file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"},
|
||||
{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.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"},
|
||||
{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.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"},
|
||||
{file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"},
|
||||
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"},
|
||||
{file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"},
|
||||
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"},
|
||||
{file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"},
|
||||
{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.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"},
|
||||
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"},
|
||||
{file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"},
|
||||
{file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"},
|
||||
{file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"},
|
||||
{file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
|
@ -1083,22 +996,6 @@ 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)"]
|
||||
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]]
|
||||
name = "prompt-toolkit"
|
||||
version = "3.0.52"
|
||||
|
|
@ -1335,28 +1232,6 @@ files = [
|
|||
{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"
|
||||
|
|
@ -1585,7 +1460,7 @@ version = "1.3.1"
|
|||
description = "Sniff out which async library your code is running under"
|
||||
optional = false
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main", "dev"]
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"},
|
||||
{file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"},
|
||||
|
|
@ -1701,18 +1576,18 @@ files = [
|
|||
{file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"},
|
||||
{file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"},
|
||||
]
|
||||
markers = {dev = "python_version < \"3.13\""}
|
||||
markers = {dev = "python_version == \"3.11\""}
|
||||
|
||||
[[package]]
|
||||
name = "typing-inspection"
|
||||
version = "0.4.2"
|
||||
version = "0.4.1"
|
||||
description = "Runtime typing introspection tools"
|
||||
optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"},
|
||||
{file = "typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464"},
|
||||
{file = "typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51"},
|
||||
{file = "typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
|
|
@ -1816,4 +1691,4 @@ auth = ["argon2-cffi", "crypto", "passlib", "pyjwt", "python-multipart"]
|
|||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = ">=3.11"
|
||||
content-hash = "cc1604995d3b73ee302e63731dd300ea17c4d95d0cfc6c386626dd9a9f60e8a7"
|
||||
content-hash = "3d301460081dada359d425d69feefc63c1e5135aa64b6f000f554bfc1231febd"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
[project]
|
||||
name = "ohmyapi"
|
||||
version = "0.6.2"
|
||||
version = "0.2.2"
|
||||
description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations"
|
||||
license = "MIT"
|
||||
keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"]
|
||||
authors = [
|
||||
{name = "Brian Wiborg", email = "me@brianwib.org"}
|
||||
]
|
||||
repository = "https://code.c-base.org/baccenfutter/ohmyapi"
|
||||
readme = "README.md"
|
||||
requires-python = ">=3.11"
|
||||
|
||||
|
|
@ -30,8 +31,6 @@ 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"]
|
||||
|
|
@ -42,22 +41,6 @@ packages = [ { include = "ohmyapi", from = "src" } ]
|
|||
[project.scripts]
|
||||
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]
|
||||
line-length = 88
|
||||
target-version = ['py39', 'py310', 'py311', 'py312', 'py313']
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__VERSION__ = "0.6.2"
|
||||
from . import db
|
||||
|
|
|
|||
|
|
@ -1,52 +1,39 @@
|
|||
from ohmyapi.db import Model, field, Q
|
||||
from ohmyapi.router import HTTPException
|
||||
|
||||
from .utils import hmac_hash
|
||||
|
||||
from datetime import datetime
|
||||
from passlib.context import CryptContext
|
||||
from typing import Optional
|
||||
from functools import wraps
|
||||
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
|
||||
|
||||
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):
|
||||
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)
|
||||
email_hash: str = field.CharField(max_length=255, unique=True, index=True)
|
||||
password_hash: str = field.CharField(max_length=128)
|
||||
is_admin: bool = field.BooleanField(default=False)
|
||||
is_staff: bool = field.BooleanField(default=False)
|
||||
created_at: datetime = field.DatetimeField(auto_now_add=True)
|
||||
updated_at: datetime = field.DatetimeField(auto_now=True)
|
||||
groups: field.ManyToManyRelation[Group] = field.ManyToManyField(
|
||||
"ohmyapi_auth.Group", related_name="users", through="usergroups"
|
||||
)
|
||||
|
||||
class Schema:
|
||||
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()])
|
||||
exclude = ("password_hash",)
|
||||
|
||||
def set_password(self, raw_password: str) -> None:
|
||||
"""Hash and store the 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:
|
||||
"""Verify a plaintext password against the stored hash."""
|
||||
return pwd_context.verify(raw_password, self.password_hash)
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
from .routes import (
|
||||
get_current_user,
|
||||
get_token,
|
||||
maybe_authenticated,
|
||||
require_admin,
|
||||
require_authenticated,
|
||||
require_group,
|
||||
|
|
|
|||
|
|
@ -1,17 +1,14 @@
|
|||
import time
|
||||
from enum import Enum
|
||||
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
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import jwt
|
||||
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 = APIRouter(prefix="/auth", tags=["Auth"])
|
||||
|
|
@ -26,25 +23,12 @@ 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_optional_scheme = OptionalOAuth2PasswordBearer(tokenUrl="/auth/login")
|
||||
|
||||
|
||||
class ClaimsUser(BaseModel):
|
||||
username: str
|
||||
email: str
|
||||
is_admin: bool
|
||||
is_staff: bool
|
||||
|
||||
|
|
@ -53,6 +37,7 @@ class Claims(BaseModel):
|
|||
type: str
|
||||
sub: str
|
||||
user: ClaimsUser
|
||||
roles: List[str]
|
||||
exp: str
|
||||
|
||||
|
||||
|
|
@ -79,15 +64,17 @@ class TokenType(str, Enum):
|
|||
refresh = "refresh"
|
||||
|
||||
|
||||
def claims(token_type: TokenType, user: User = []) -> Claims:
|
||||
def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Claims:
|
||||
return Claims(
|
||||
type=token_type,
|
||||
sub=str(user.id),
|
||||
user=ClaimsUser(
|
||||
username=user.username,
|
||||
email=user.email,
|
||||
is_admin=user.is_admin,
|
||||
is_staff=user.is_staff,
|
||||
),
|
||||
roles=[g.name for g in groups],
|
||||
exp="",
|
||||
)
|
||||
|
||||
|
|
@ -137,12 +124,6 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> 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:
|
||||
"""Ensure the current user is an admin."""
|
||||
if not current_user:
|
||||
|
|
@ -199,25 +180,17 @@ async def login(form_data: LoginRequest = Body(...)):
|
|||
)
|
||||
|
||||
|
||||
class TokenRefresh(BaseModel):
|
||||
refresh_token: str
|
||||
|
||||
|
||||
@router.post("/refresh", response_model=AccessToken)
|
||||
async def refresh_token(refresh_token: TokenRefresh = Body(...)):
|
||||
async def refresh_token(refresh_token: str):
|
||||
"""Exchange refresh token for new access token."""
|
||||
payload = decode_token(refresh_token.refresh_token)
|
||||
payload = decode_token(refresh_token)
|
||||
if payload.get("type") != "refresh":
|
||||
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)
|
||||
|
||||
user = await User.filter(id=user_id).first()
|
||||
if not user:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"
|
||||
|
|
@ -226,7 +199,7 @@ async def refresh_token(refresh_token: TokenRefresh = Body(...)):
|
|||
new_access = create_token(
|
||||
claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS
|
||||
)
|
||||
return AccessToken(token_type="bearer", access_token=new_access)
|
||||
return AccessToken(token_type="bearer", access_token=access_token)
|
||||
|
||||
|
||||
@router.get("/introspect", response_model=Dict[str, Any])
|
||||
|
|
|
|||
|
|
@ -1,17 +0,0 @@
|
|||
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")
|
||||
|
||||
|
|
@ -9,9 +9,6 @@ import typer
|
|||
import uvicorn
|
||||
|
||||
from ohmyapi.core import runtime, scaffolding
|
||||
from ohmyapi.core.logging import setup_logging
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
app = typer.Typer(
|
||||
help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM."
|
||||
|
|
@ -22,24 +19,12 @@ app = typer.Typer(
|
|||
def startproject(name: str):
|
||||
"""Create a new OhMyAPI project in the given directory."""
|
||||
scaffolding.startproject(name)
|
||||
logger.info(f"✅ Project '{name}' created successfully.")
|
||||
logger.info(f"🔧 Next, configure your project in {name}/settings.py")
|
||||
|
||||
|
||||
@app.command()
|
||||
def startapp(app_name: str, root: str = "."):
|
||||
"""Create a new app with the given name in your OhMyAPI project."""
|
||||
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()
|
||||
|
|
@ -49,13 +34,12 @@ def serve(root: str = ".", host="127.0.0.1", port=8000):
|
|||
"""
|
||||
project_path = Path(root)
|
||||
project = runtime.Project(project_path)
|
||||
app_instance = project.configure_app(project.app())
|
||||
uvicorn.run(app_instance, host=host, port=int(port), reload=False, log_config=None)
|
||||
app_instance = project.app()
|
||||
uvicorn.run(app_instance, host=host, port=int(port), reload=False)
|
||||
|
||||
|
||||
@app.command()
|
||||
def shell(root: str = "."):
|
||||
"""An interactive shell with your loaded project runtime."""
|
||||
project_path = Path(root).resolve()
|
||||
project = runtime.Project(project_path)
|
||||
|
||||
|
|
@ -83,7 +67,7 @@ def shell(root: str = "."):
|
|||
asyncio.set_event_loop(loop)
|
||||
loop.run_until_complete(init_and_cleanup())
|
||||
|
||||
# Prepare shell vars that are to be immediately available
|
||||
# Prepare shell vars that are to be directly available
|
||||
shell_vars = {"p": project}
|
||||
|
||||
try:
|
||||
|
|
@ -115,8 +99,6 @@ def makemigrations(app: str = "*", name: str = "auto", root: str = "."):
|
|||
asyncio.run(project.makemigrations(app_label=app, name=name))
|
||||
else:
|
||||
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()
|
||||
|
|
@ -131,7 +113,6 @@ def migrate(app: str = "*", root: str = "."):
|
|||
asyncio.run(project.migrate(app))
|
||||
else:
|
||||
asyncio.run(project.migrate(app))
|
||||
logger.info(f"✅ Migrations ran successfully.")
|
||||
|
||||
|
||||
@app.command()
|
||||
|
|
@ -161,17 +142,9 @@ def createsuperuser(root: str = "."):
|
|||
if password1 != password2:
|
||||
print("Passwords didn't match!")
|
||||
user = ohmyapi_auth.models.User(
|
||||
username=username, is_staff=True, is_admin=True
|
||||
email=email, 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.set_email(email)
|
||||
user.set_password(password1)
|
||||
await user.save()
|
||||
await project.close_orm()
|
||||
|
||||
asyncio.run(_run())
|
||||
asyncio.run(project.init_orm())
|
||||
asyncio.run(user.save())
|
||||
asyncio.run(project.close_orm())
|
||||
|
|
|
|||
|
|
@ -1,38 +0,0 @@
|
|||
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
|
||||
|
|
@ -7,7 +7,7 @@ import sys
|
|||
from http import HTTPStatus
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import Any, Dict, Generator, List, Optional, Tuple, Type
|
||||
from typing import Any, Dict, Generator, List, Optional, Type
|
||||
|
||||
import click
|
||||
from aerich import Command as AerichCommand
|
||||
|
|
@ -15,11 +15,8 @@ from aerich.exceptions import NotInitedError
|
|||
from fastapi import APIRouter, FastAPI
|
||||
from tortoise import Tortoise
|
||||
|
||||
from ohmyapi.core.logging import setup_logging
|
||||
from ohmyapi.db.model import Model
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class Project:
|
||||
"""
|
||||
|
|
@ -32,7 +29,6 @@ class Project:
|
|||
"""
|
||||
|
||||
def __init__(self, project_path: str):
|
||||
logger.debug(f"Loading project: {project_path}")
|
||||
self.project_path = Path(project_path).resolve()
|
||||
self._apps: Dict[str, App] = {}
|
||||
self.migrations_dir = self.project_path / "migrations"
|
||||
|
|
@ -40,14 +36,6 @@ class Project:
|
|||
if str(self.project_path) not in sys.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>.
|
||||
# We need this, because Tortoise app-names may not include dots `.`.
|
||||
spec = importlib.util.find_spec("ohmyapi.builtin")
|
||||
|
|
@ -65,6 +53,14 @@ class Project:
|
|||
except ModuleNotFoundError:
|
||||
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
|
||||
for app_name in getattr(self.settings, "INSTALLED_APPS", []):
|
||||
self._apps[app_name] = App(self, name=app_name)
|
||||
|
|
@ -76,39 +72,23 @@ class Project:
|
|||
def is_app_installed(self, name: str) -> bool:
|
||||
return name in getattr(self.settings, "INSTALLED_APPS", [])
|
||||
|
||||
def app(self,
|
||||
docs_url: str = "/docs",
|
||||
) -> FastAPI:
|
||||
def app(self, generate_schemas: bool = False) -> FastAPI:
|
||||
"""
|
||||
Create and return a FastAPI app.
|
||||
Create a FastAPI app, attach all APIRouters from registered apps,
|
||||
and register ORM lifecycle event handlers.
|
||||
"""
|
||||
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__,
|
||||
)
|
||||
app = FastAPI(title=getattr(self.settings, "PROJECT_NAME", "OhMyAPI Project"))
|
||||
|
||||
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.
|
||||
# Attach routers from apps
|
||||
for app_name, app_def in self._apps.items():
|
||||
for middleware, kwargs in app_def.middlewares:
|
||||
app.add_middleware(middleware, **kwargs)
|
||||
for router in app_def.routers:
|
||||
app.include_router(router)
|
||||
if app_def.router:
|
||||
app.include_router(app_def.router)
|
||||
|
||||
# Initialize ORM on startup
|
||||
# Startup / shutdown events
|
||||
@app.on_event("startup")
|
||||
async def _startup():
|
||||
await self.init_orm(generate_schemas=False)
|
||||
await self.init_orm(generate_schemas=generate_schemas)
|
||||
|
||||
# Close ORM on shutdown
|
||||
@app.on_event("shutdown")
|
||||
async def _shutdown():
|
||||
await self.close_orm()
|
||||
|
|
@ -129,7 +109,7 @@ class Project:
|
|||
}
|
||||
|
||||
for app_name, app in self._apps.items():
|
||||
modules = list(app._models.keys())
|
||||
modules = list(app.models.keys())
|
||||
if modules:
|
||||
config["apps"][app_name] = {
|
||||
"models": modules,
|
||||
|
|
@ -154,10 +134,7 @@ class Project:
|
|||
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
|
||||
tortoise_cfg["apps"][app_label]["models"].append("aerich.models")
|
||||
|
|
@ -240,16 +217,11 @@ class App:
|
|||
# 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)
|
||||
|
|
@ -267,6 +239,7 @@ class App:
|
|||
try:
|
||||
importlib.import_module(mod_name)
|
||||
except ModuleNotFoundError:
|
||||
print(f"no models detected: {mod_name}")
|
||||
return
|
||||
|
||||
# Acoid duplicates.
|
||||
|
|
@ -284,11 +257,7 @@ class App:
|
|||
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__"):
|
||||
|
|
@ -309,7 +278,8 @@ class App:
|
|||
# An app may come without any routes.
|
||||
try:
|
||||
importlib.import_module(mod_name)
|
||||
except ModuleNotFoundError:
|
||||
except ModuleNotFound:
|
||||
print(f"no routes detected: {mod_name}")
|
||||
return
|
||||
|
||||
# Avoid duplicates.
|
||||
|
|
@ -324,29 +294,18 @@ class App:
|
|||
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)
|
||||
submod = importlib.import_module(subname)
|
||||
walk(submod)
|
||||
|
||||
# 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.
|
||||
|
|
@ -360,12 +319,6 @@ class App:
|
|||
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]:
|
||||
"""
|
||||
|
|
@ -375,44 +328,28 @@ class App:
|
|||
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
|
||||
return {
|
||||
module: 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
|
||||
router = APIRouter()
|
||||
for routes_mod in self._routers:
|
||||
for r in self._routers[routes_mod]:
|
||||
router.include_router(r)
|
||||
return router.routes
|
||||
|
||||
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(),
|
||||
"models": [
|
||||
f"{self.name}.{m.__name__}" for m in self.models[f"{self.name}.models"]
|
||||
],
|
||||
"routes": self.__serialize_router(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,16 +2,10 @@ from pathlib import Path
|
|||
|
||||
from jinja2 import Environment, FileSystemLoader
|
||||
|
||||
from ohmyapi.core.logging import setup_logging
|
||||
|
||||
import shutil
|
||||
|
||||
# Base templates directory
|
||||
TEMPLATE_DIR = Path(__file__).parent / "templates"
|
||||
env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
def render_template_file(template_path: Path, context: dict, output_path: Path):
|
||||
"""Render a single Jinja2 template file to disk."""
|
||||
|
|
@ -59,29 +53,13 @@ def render_template_dir(
|
|||
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):
|
||||
"""Create a new project: flat structure, all project templates go into <name>/"""
|
||||
target_dir = Path(name).resolve()
|
||||
target_dir.mkdir(exist_ok=True)
|
||||
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):
|
||||
|
|
@ -94,3 +72,5 @@ def startapp(name: str, project: str):
|
|||
{"project_name": target_dir.resolve().name, "app_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!")
|
||||
|
|
|
|||
|
|
@ -1,29 +0,0 @@
|
|||
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"]
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
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
|
||||
|
|
@ -10,7 +10,7 @@ readme = "README.md"
|
|||
license = { text = "MIT" }
|
||||
|
||||
dependencies = [
|
||||
# "asyncpg"
|
||||
"ohmyapi (>=0.1.0,<0.2.0)"
|
||||
]
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
from tortoise.expressions import Q
|
||||
from tortoise.manager import Manager
|
||||
from tortoise.query_utils import Prefetch
|
||||
from tortoise.queryset import QuerySet
|
||||
from tortoise.signals import (
|
||||
post_delete,
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
from tortoise.functions import *
|
||||
|
|
@ -31,18 +31,6 @@ UUID.__get_pydantic_core_schema__ = classmethod(__uuid_schema_monkey_patch)
|
|||
|
||||
class ModelMeta(type(TortoiseModel)):
|
||||
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.
|
||||
schema_opts = attrs.get("Schema", None)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,26 +0,0 @@
|
|||
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),
|
||||
}
|
||||
)
|
||||
Loading…
Add table
Add a link
Reference in a new issue