Compare commits
No commits in common. "main" and "0.4.4" have entirely different histories.
14 changed files with 82 additions and 237 deletions
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -40,6 +40,6 @@ router = APIRouter()
|
|||
|
||||
|
||||
@router.get("/")
|
||||
def get(user: auth.User = Depends(permissions.require_authenticated)):
|
||||
def get(user: auth.User = Depends(permissions.required_authenticated)):
|
||||
...
|
||||
```
|
||||
|
|
|
|||
113
poetry.lock
generated
113
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,18 +564,6 @@ 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"
|
||||
|
|
@ -1083,22 +1024,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 +1260,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 +1488,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,7 +1604,7 @@ 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"
|
||||
|
|
@ -1816,4 +1719,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,6 +1,6 @@
|
|||
[project]
|
||||
name = "ohmyapi"
|
||||
version = "0.6.2"
|
||||
version = "0.4.4"
|
||||
description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations"
|
||||
license = "MIT"
|
||||
keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"]
|
||||
|
|
@ -30,8 +30,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"]
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
__VERSION__ = "0.6.2"
|
||||
__VERSION__ = "0.4.4"
|
||||
|
|
|
|||
|
|
@ -1,39 +1,53 @@
|
|||
import hmac
|
||||
import hashlib
|
||||
import base64
|
||||
from functools import wraps
|
||||
from secrets import token_bytes
|
||||
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, 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 uuid import UUID
|
||||
import settings
|
||||
|
||||
pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
||||
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")
|
||||
|
||||
|
||||
class Group(Model):
|
||||
id: UUID = field.data.UUIDField(pk=True)
|
||||
name: str = field.CharField(max_length=42, index=True)
|
||||
|
||||
def __str__(self):
|
||||
return self.name if self.name else ""
|
||||
|
||||
|
||||
class User(Model):
|
||||
id: UUID = field.data.UUIDField(pk=True)
|
||||
username: str = field.CharField(max_length=150, unique=True)
|
||||
email_hash: str = field.CharField(max_length=255, unique=True, index=True)
|
||||
email_hash: Optional[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",
|
||||
}
|
||||
exclude = ["password_hash", "email_hash"]
|
||||
|
||||
def __str__(self):
|
||||
fields = {
|
||||
'username': self.username,
|
||||
'username': self.username if self.username else "-",
|
||||
'is_admin': 'y' if self.is_admin else 'n',
|
||||
'is_staff': 'y' if self.is_staff else 'n',
|
||||
}
|
||||
|
|
@ -52,7 +66,7 @@ class User(Model):
|
|||
return pwd_context.verify(raw_password, self.password_hash)
|
||||
|
||||
@classmethod
|
||||
async def authenticate(cls, username: str, password: str) -> Optional["User"]:
|
||||
async def authenticate_username(cls, username: str, password: str) -> Optional["User"]:
|
||||
"""Authenticate a user by username and password."""
|
||||
user = await cls.filter(username=username).first()
|
||||
if user and user.verify_password(password):
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
||||
|
|
@ -165,13 +165,6 @@ def createsuperuser(root: str = "."):
|
|||
)
|
||||
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())
|
||||
|
|
|
|||
|
|
@ -40,14 +40,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 +57,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)
|
||||
|
|
@ -342,9 +342,9 @@ class App:
|
|||
except ModuleNotFoundError:
|
||||
return
|
||||
|
||||
installer = getattr(mod, "install", None)
|
||||
if installer is not None:
|
||||
for middleware in installer():
|
||||
getter = getattr(mod, "get", None)
|
||||
if getter is not None:
|
||||
for middleware in getter():
|
||||
self._middlewares.append(middleware)
|
||||
|
||||
def __serialize_route(self, route):
|
||||
|
|
@ -404,15 +404,10 @@ class App:
|
|||
"""
|
||||
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,
|
||||
"models": [
|
||||
f"{self.name}.{m.__name__}" for m in self._models[f"{self.name}.models"]
|
||||
],
|
||||
"middlewares": self.__serialize_middleware(),
|
||||
"routes": self.__serialize_router(),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -15,12 +15,12 @@ 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,
|
||||
{
|
||||
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