ohmyapi 0.1.27__tar.gz → 0.2.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/PKG-INFO +1 -1
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/pyproject.toml +1 -1
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/builtin/auth/models.py +1 -1
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/builtin/auth/routes.py +1 -1
- ohmyapi-0.2.1/src/ohmyapi/builtin/demo/__init__.py +1 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/builtin/demo/models.py +4 -3
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/builtin/demo/routes.py +5 -9
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/runtime.py +141 -60
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/db/model/model.py +5 -3
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/router.py +2 -1
- ohmyapi-0.1.27/src/ohmyapi/builtin/demo/__init__.py +0 -2
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/README.md +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/__init__.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/__main__.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/builtin/auth/__init__.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/builtin/auth/permissions.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/cli.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/__init__.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/scaffolding.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/templates/app/__init__.py.j2 +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/templates/app/models.py.j2 +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/templates/app/routes.py.j2 +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/templates/project/README.md.j2 +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/templates/project/pyproject.toml.j2 +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/core/templates/project/settings.py.j2 +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/db/__init__.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/db/exceptions.py +0 -0
- {ohmyapi-0.1.27 → ohmyapi-0.2.1}/src/ohmyapi/db/model/__init__.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ohmyapi
|
3
|
-
Version: 0.1
|
3
|
+
Version: 0.2.1
|
4
4
|
Summary: Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations
|
5
5
|
License-Expression: MIT
|
6
6
|
Keywords: fastapi,tortoise,orm,pydantic,async,web-framework
|
@@ -1,6 +1,6 @@
|
|
1
1
|
[project]
|
2
2
|
name = "ohmyapi"
|
3
|
-
version = "0.1
|
3
|
+
version = "0.2.1"
|
4
4
|
description = "Django-flavored scaffolding and management layer around FastAPI, Pydantic, TortoiseORM and Aerich migrations"
|
5
5
|
license = "MIT"
|
6
6
|
keywords = ["fastapi", "tortoise", "orm", "pydantic", "async", "web-framework"]
|
@@ -24,7 +24,7 @@ class User(Model):
|
|
24
24
|
is_admin: bool = field.BooleanField(default=False)
|
25
25
|
is_staff: bool = field.BooleanField(default=False)
|
26
26
|
groups: field.ManyToManyRelation[Group] = field.ManyToManyField(
|
27
|
-
"ohmyapi_auth.Group", related_name="users", through="
|
27
|
+
"ohmyapi_auth.Group", related_name="users", through="usergroups"
|
28
28
|
)
|
29
29
|
|
30
30
|
class Schema:
|
@@ -11,7 +11,7 @@ from pydantic import BaseModel
|
|
11
11
|
from ohmyapi.builtin.auth.models import Group, User
|
12
12
|
|
13
13
|
# Router
|
14
|
-
router = APIRouter(prefix="/auth", tags=["
|
14
|
+
router = APIRouter(prefix="/auth", tags=["Auth"])
|
15
15
|
|
16
16
|
# Secrets & config (should come from settings/env in real projects)
|
17
17
|
JWT_SECRET = getattr(settings, "JWT_SECRET", "changeme")
|
@@ -0,0 +1 @@
|
|
1
|
+
from . import models, routes
|
@@ -1,10 +1,11 @@
|
|
1
|
-
from ohmyapi.db import Model, field
|
2
|
-
from ohmyapi_auth.models import User
|
3
|
-
|
4
1
|
from datetime import datetime
|
5
2
|
from decimal import Decimal
|
6
3
|
from uuid import UUID
|
7
4
|
|
5
|
+
from ohmyapi_auth.models import User
|
6
|
+
|
7
|
+
from ohmyapi.db import Model, field
|
8
|
+
|
8
9
|
|
9
10
|
class Team(Model):
|
10
11
|
id: UUID = field.data.UUIDField(primary_key=True)
|
@@ -1,19 +1,17 @@
|
|
1
|
-
from
|
1
|
+
from typing import List
|
2
|
+
|
2
3
|
from ohmyapi.db.exceptions import DoesNotExist
|
4
|
+
from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
|
3
5
|
|
4
6
|
from . import models
|
5
7
|
|
6
|
-
from typing import List
|
7
|
-
|
8
8
|
# Expose your app's routes via `router = fastapi.APIRouter`.
|
9
9
|
# Use prefixes wisely to avoid cross-app namespace-collisions.
|
10
10
|
# Tags improve the UX of the OpenAPI docs at /docs.
|
11
11
|
router = APIRouter(prefix="/tournament")
|
12
12
|
|
13
13
|
|
14
|
-
@router.get(
|
15
|
-
"/", tags=["tournament"], response_model=List[models.Tournament.Schema()]
|
16
|
-
)
|
14
|
+
@router.get("/", tags=["tournament"], response_model=List[models.Tournament.Schema()])
|
17
15
|
async def list():
|
18
16
|
"""List all tournaments."""
|
19
17
|
return await models.Tournament.Schema().from_queryset(models.Tournament.all())
|
@@ -30,9 +28,7 @@ async def post(tournament: models.Tournament.Schema(readonly=True)):
|
|
30
28
|
@router.get("/{id}", tags=["tournament"], response_model=models.Tournament.Schema())
|
31
29
|
async def get(id: str):
|
32
30
|
"""Get tournament by id."""
|
33
|
-
return await models.Tournament.Schema().from_queryset(
|
34
|
-
models.Tournament.get(id=id)
|
35
|
-
)
|
31
|
+
return await models.Tournament.Schema().from_queryset(models.Tournament.get(id=id))
|
36
32
|
|
37
33
|
|
38
34
|
@router.put(
|
@@ -1,12 +1,13 @@
|
|
1
1
|
# ohmyapi/core/runtime.py
|
2
|
-
import copy
|
3
2
|
import importlib
|
4
3
|
import importlib.util
|
5
4
|
import json
|
6
5
|
import pkgutil
|
7
6
|
import sys
|
7
|
+
from http import HTTPStatus
|
8
8
|
from pathlib import Path
|
9
|
-
from
|
9
|
+
from types import ModuleType
|
10
|
+
from typing import Any, Dict, Generator, List, Optional, Type
|
10
11
|
|
11
12
|
import click
|
12
13
|
from aerich import Command as AerichCommand
|
@@ -21,8 +22,9 @@ class Project:
|
|
21
22
|
"""
|
22
23
|
Project runtime loader + Tortoise/Aerich integration.
|
23
24
|
|
24
|
-
-
|
25
|
-
-
|
25
|
+
- aliases builtin apps as ohmyapi_<name>
|
26
|
+
- loads all INSTALLED_APPS into scope
|
27
|
+
- builds unified tortoise config for ORM runtime
|
26
28
|
- provides makemigrations/migrate methods using Aerich Command API
|
27
29
|
"""
|
28
30
|
|
@@ -34,8 +36,8 @@ class Project:
|
|
34
36
|
if str(self.project_path) not in sys.path:
|
35
37
|
sys.path.insert(0, str(self.project_path))
|
36
38
|
|
37
|
-
#
|
38
|
-
#
|
39
|
+
# Alias builtin apps as ohmyapi_<name>.
|
40
|
+
# We need this, because Tortoise app-names may not include dots `.`.
|
39
41
|
spec = importlib.util.find_spec("ohmyapi.builtin")
|
40
42
|
if spec and spec.submodule_search_locations:
|
41
43
|
for _, modname, _ in pkgutil.iter_modules(spec.submodule_search_locations):
|
@@ -107,7 +109,7 @@ class Project:
|
|
107
109
|
}
|
108
110
|
|
109
111
|
for app_name, app in self._apps.items():
|
110
|
-
modules = list(
|
112
|
+
modules = list(app.models.keys())
|
111
113
|
if modules:
|
112
114
|
config["apps"][app_name] = {
|
113
115
|
"models": modules,
|
@@ -119,11 +121,20 @@ class Project:
|
|
119
121
|
def build_aerich_command(
|
120
122
|
self, app_label: str, db_url: Optional[str] = None
|
121
123
|
) -> AerichCommand:
|
124
|
+
"""
|
125
|
+
Build Aerich command for app with given app_label.
|
126
|
+
|
127
|
+
Aerich needs to see only the app of interest, but with the extra model
|
128
|
+
"aerich.models".
|
129
|
+
"""
|
122
130
|
if app_label not in self._apps:
|
123
131
|
raise RuntimeError(f"App '{app_label}' is not registered")
|
124
132
|
|
125
133
|
# Get a fresh copy of the config (without aerich.models anywhere)
|
126
|
-
tortoise_cfg =
|
134
|
+
tortoise_cfg = self.build_tortoise_config(db_url=db_url)
|
135
|
+
|
136
|
+
# Prevent leaking other app's models to Aerich.
|
137
|
+
tortoise_cfg["apps"] = {app_label: tortoise_cfg["apps"][app_label]}
|
127
138
|
|
128
139
|
# Append aerich.models to the models list of the target app only
|
129
140
|
tortoise_cfg["apps"][app_label]["models"].append("aerich.models")
|
@@ -199,33 +210,18 @@ class App:
|
|
199
210
|
self.project = project
|
200
211
|
self.name = name
|
201
212
|
|
202
|
-
#
|
203
|
-
|
213
|
+
# Reference to this app's models modules. Tortoise needs to know the
|
214
|
+
# modules where to lookup models for this app.
|
215
|
+
self._models: Dict[str, ModuleType] = {}
|
204
216
|
|
205
|
-
#
|
206
|
-
self.
|
217
|
+
# Reference to this app's routes modules.
|
218
|
+
self._routers: Dict[str, ModuleType] = {}
|
207
219
|
|
208
220
|
# Import the app, so its __init__.py runs.
|
209
|
-
importlib.import_module(
|
210
|
-
|
211
|
-
# Load the models
|
212
|
-
try:
|
213
|
-
models_mod = importlib.import_module(f"{self.name}.models")
|
214
|
-
self.model_modules.append(f"{self.name}.models")
|
215
|
-
except ModuleNotFoundError:
|
216
|
-
pass
|
221
|
+
mod: ModuleType = importlib.import_module(name)
|
217
222
|
|
218
|
-
|
219
|
-
|
220
|
-
routes_mod = importlib.import_module(f"{self.name}.routes")
|
221
|
-
for attr_name in dir(routes_mod):
|
222
|
-
if attr_name.startswith("__"):
|
223
|
-
continue
|
224
|
-
attr = getattr(routes_mod, attr_name)
|
225
|
-
if isinstance(attr, APIRouter):
|
226
|
-
self.router.include_router(attr)
|
227
|
-
except ModuleNotFoundError:
|
228
|
-
pass
|
223
|
+
self.__load_models(f"{self.name}.models")
|
224
|
+
self.__load_routes(f"{self.name}.routes")
|
229
225
|
|
230
226
|
def __repr__(self):
|
231
227
|
return json.dumps(self.dict(), indent=2)
|
@@ -233,42 +229,127 @@ class App:
|
|
233
229
|
def __str__(self):
|
234
230
|
return self.__repr__()
|
235
231
|
|
236
|
-
def
|
237
|
-
"""
|
232
|
+
def __load_models(self, mod_name: str):
|
233
|
+
"""
|
234
|
+
Recursively scan through a module and collect all models.
|
235
|
+
If the module is a package, iterate through its submodules.
|
236
|
+
"""
|
237
|
+
|
238
|
+
# An app may come without any models.
|
239
|
+
try:
|
240
|
+
importlib.import_module(mod_name)
|
241
|
+
except ModuleNotFoundError:
|
242
|
+
print(f"no models detected: {mod_name}")
|
243
|
+
return
|
244
|
+
|
245
|
+
# Acoid duplicates.
|
246
|
+
visited: set[str] = set()
|
247
|
+
|
248
|
+
def walk(mod_name: str):
|
249
|
+
mod = importlib.import_module(mod_name)
|
250
|
+
if mod_name in visited:
|
251
|
+
return
|
252
|
+
visited.add(mod_name)
|
253
|
+
|
254
|
+
for name, value in vars(mod).copy().items():
|
255
|
+
if (
|
256
|
+
isinstance(value, type)
|
257
|
+
and issubclass(value, Model)
|
258
|
+
and not name == Model.__name__
|
259
|
+
):
|
260
|
+
self._models[mod_name] = self._models.get(mod_name, []) + [value]
|
261
|
+
|
262
|
+
# if it's a package, recurse into submodules
|
263
|
+
if hasattr(mod, "__path__"):
|
264
|
+
for _, subname, _ in pkgutil.iter_modules(
|
265
|
+
mod.__path__, mod.__name__ + "."
|
266
|
+
):
|
267
|
+
walk(subname)
|
268
|
+
|
269
|
+
# Walk the walk.
|
270
|
+
walk(mod_name)
|
271
|
+
|
272
|
+
def __load_routes(self, mod_name: str):
|
273
|
+
"""
|
274
|
+
Recursively scan through a module and collect all APIRouters.
|
275
|
+
If the module is a package, iterate through all its submodules.
|
276
|
+
"""
|
277
|
+
|
278
|
+
# An app may come without any routes.
|
279
|
+
try:
|
280
|
+
importlib.import_module(mod_name)
|
281
|
+
except ModuleNotFound:
|
282
|
+
print(f"no routes detected: {mod_name}")
|
283
|
+
return
|
284
|
+
|
285
|
+
# Avoid duplicates.
|
286
|
+
visited: set[str] = set()
|
287
|
+
|
288
|
+
def walk(mod_name: str):
|
289
|
+
mod = importlib.import_module(mod_name)
|
290
|
+
if mod.__name__ in visited:
|
291
|
+
return
|
292
|
+
visited.add(mod.__name__)
|
293
|
+
|
294
|
+
for name, value in vars(mod).copy().items():
|
295
|
+
if isinstance(value, APIRouter) and not name == APIRouter.__name__:
|
296
|
+
self._routers[mod_name] = self._routers.get(mod_name, []) + [value]
|
297
|
+
|
298
|
+
# if it's a package, recurse into submodules
|
299
|
+
if hasattr(mod, "__path__"):
|
300
|
+
for _, subname, _ in pkgutil.iter_modules(
|
301
|
+
mod.__path__, mod.__name__ + "."
|
302
|
+
):
|
303
|
+
submod = importlib.import_module(subname)
|
304
|
+
walk(submod)
|
305
|
+
|
306
|
+
# Walk the walk.
|
307
|
+
walk(mod_name)
|
308
|
+
|
309
|
+
def __serialize_route(self, route):
|
310
|
+
"""
|
311
|
+
Convert APIRoute to JSON-serializable dict.
|
312
|
+
"""
|
238
313
|
return {
|
239
314
|
"path": route.path,
|
240
|
-
"
|
241
|
-
"
|
242
|
-
"endpoint": route.endpoint.__name__, # just the function name
|
243
|
-
"response_model": (
|
244
|
-
getattr(route, "response_model", None).__name__
|
245
|
-
if getattr(route, "response_model", None)
|
246
|
-
else None
|
247
|
-
),
|
248
|
-
"tags": getattr(route, "tags", None),
|
315
|
+
"method": list(route.methods)[0],
|
316
|
+
"endpoint": f"{route.endpoint.__module__}.{route.endpoint.__name__}",
|
249
317
|
}
|
250
318
|
|
251
|
-
def
|
252
|
-
return [self.
|
319
|
+
def __serialize_router(self):
|
320
|
+
return [self.__serialize_route(route) for route in self.routes]
|
253
321
|
|
254
|
-
|
322
|
+
@property
|
323
|
+
def models(self) -> List[ModuleType]:
|
324
|
+
"""
|
325
|
+
Return a list of all loaded models.
|
326
|
+
"""
|
327
|
+
out = []
|
328
|
+
for module in self._models:
|
329
|
+
for model in self._models[module]:
|
330
|
+
out.append(model)
|
255
331
|
return {
|
256
|
-
|
257
|
-
"routes": self._serialize_router(),
|
332
|
+
module: out,
|
258
333
|
}
|
259
334
|
|
260
|
-
@property
|
261
|
-
def models(self) -> Generator[Model, None, None]:
|
262
|
-
for mod in self.model_modules:
|
263
|
-
models_mod = importlib.import_module(mod)
|
264
|
-
for obj in models_mod.__dict__.values():
|
265
|
-
if (
|
266
|
-
isinstance(obj, type)
|
267
|
-
and getattr(obj, "_meta", None) is not None
|
268
|
-
and obj.__name__ != "Model"
|
269
|
-
):
|
270
|
-
yield obj
|
271
|
-
|
272
335
|
@property
|
273
336
|
def routes(self):
|
274
|
-
|
337
|
+
"""
|
338
|
+
Return an APIRouter with all loaded routes.
|
339
|
+
"""
|
340
|
+
router = APIRouter()
|
341
|
+
for routes_mod in self._routers:
|
342
|
+
for r in self._routers[routes_mod]:
|
343
|
+
router.include_router(r)
|
344
|
+
return router.routes
|
345
|
+
|
346
|
+
def dict(self) -> Dict[str, Any]:
|
347
|
+
"""
|
348
|
+
Convenience method for serializing the runtime data.
|
349
|
+
"""
|
350
|
+
return {
|
351
|
+
"models": [
|
352
|
+
f"{self.name}.{m.__name__}" for m in self.models[f"{self.name}.models"]
|
353
|
+
],
|
354
|
+
"routes": self.__serialize_router(),
|
355
|
+
}
|
@@ -30,10 +30,12 @@ UUID.__get_pydantic_core_schema__ = classmethod(__uuid_schema_monkey_patch)
|
|
30
30
|
|
31
31
|
|
32
32
|
class ModelMeta(type(TortoiseModel)):
|
33
|
-
def __new__(
|
34
|
-
|
33
|
+
def __new__(mcls, name, bases, attrs):
|
34
|
+
# Grab the Schema class for further processing.
|
35
|
+
schema_opts = attrs.get("Schema", None)
|
35
36
|
|
36
|
-
|
37
|
+
# Let Tortoise's Metaclass do it's thing.
|
38
|
+
new_cls = super().__new__(mcls, name, bases, attrs)
|
37
39
|
|
38
40
|
class BoundSchema:
|
39
41
|
def __call__(self, readonly: bool = False):
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|
File without changes
|