ohmyapi 0.1.7__py3-none-any.whl → 0.1.9__py3-none-any.whl
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/builtin/auth/models.py +2 -2
- ohmyapi/builtin/auth/permissions.py +1 -0
- ohmyapi/builtin/auth/routes.py +46 -14
- ohmyapi/cli.py +7 -6
- {ohmyapi-0.1.7.dist-info → ohmyapi-0.1.9.dist-info}/METADATA +55 -20
- {ohmyapi-0.1.7.dist-info → ohmyapi-0.1.9.dist-info}/RECORD +8 -8
- {ohmyapi-0.1.7.dist-info → ohmyapi-0.1.9.dist-info}/WHEEL +0 -0
- {ohmyapi-0.1.7.dist-info → ohmyapi-0.1.9.dist-info}/entry_points.txt +0 -0
ohmyapi/builtin/auth/models.py
CHANGED
@@ -7,12 +7,12 @@ pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
|
|
7
7
|
|
8
8
|
|
9
9
|
class Group(Model):
|
10
|
-
id = field.
|
10
|
+
id = field.data.UUIDField(pk=True)
|
11
11
|
name = field.CharField(max_length=42, index=True)
|
12
12
|
|
13
13
|
|
14
14
|
class User(Model):
|
15
|
-
id = field.
|
15
|
+
id = field.data.UUIDField(pk=True)
|
16
16
|
email = field.CharField(max_length=255, unique=True, index=True)
|
17
17
|
username = field.CharField(max_length=150, unique=True)
|
18
18
|
password_hash = field.CharField(max_length=128)
|
ohmyapi/builtin/auth/routes.py
CHANGED
@@ -1,12 +1,13 @@
|
|
1
1
|
import time
|
2
|
-
from
|
2
|
+
from enum import Enum
|
3
|
+
from typing import Any, Dict, List
|
3
4
|
|
4
5
|
import jwt
|
5
6
|
from fastapi import APIRouter, Body, Depends, Header, HTTPException, status
|
6
7
|
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
7
8
|
from pydantic import BaseModel
|
8
9
|
|
9
|
-
from ohmyapi.builtin.auth.models import User
|
10
|
+
from ohmyapi.builtin.auth.models import User, Group
|
10
11
|
|
11
12
|
import settings
|
12
13
|
|
@@ -40,14 +41,39 @@ def decode_token(token: str) -> Dict:
|
|
40
41
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
|
41
42
|
|
42
43
|
|
44
|
+
class TokenType(str, Enum):
|
45
|
+
"""
|
46
|
+
Helper for indicating the token type when generating claims.
|
47
|
+
"""
|
48
|
+
access = "access"
|
49
|
+
refresh = "refresh"
|
50
|
+
|
51
|
+
|
52
|
+
def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Dict[str, Any]:
|
53
|
+
return {
|
54
|
+
'type': token_type,
|
55
|
+
'sub': str(user.id),
|
56
|
+
'user': {
|
57
|
+
'username': user.username,
|
58
|
+
'email': user.email,
|
59
|
+
},
|
60
|
+
'roles': [g.name for g in groups]
|
61
|
+
}
|
62
|
+
|
63
|
+
async def get_token(token: str = Depends(oauth2_scheme)) -> Dict:
|
64
|
+
"""Dependency: token introspection"""
|
65
|
+
payload = decode_token(token)
|
66
|
+
return payload
|
67
|
+
|
68
|
+
|
43
69
|
async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
|
44
70
|
"""Dependency: extract user from access token."""
|
45
71
|
payload = decode_token(token)
|
46
|
-
|
47
|
-
if
|
72
|
+
user_id = payload.get("sub")
|
73
|
+
if user_id is None:
|
48
74
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
|
49
75
|
|
50
|
-
user = await User.filter(
|
76
|
+
user = await User.filter(id=user_id).first()
|
51
77
|
if not user:
|
52
78
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
53
79
|
return user
|
@@ -100,8 +126,8 @@ async def login(form_data: LoginRequest = Body(...)):
|
|
100
126
|
if not user:
|
101
127
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
|
102
128
|
|
103
|
-
access_token = create_token(
|
104
|
-
refresh_token = create_token(
|
129
|
+
access_token = create_token(claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS)
|
130
|
+
refresh_token = create_token(claims(TokenType.refresh, user), REFRESH_TOKEN_EXPIRE_SECONDS)
|
105
131
|
|
106
132
|
return {
|
107
133
|
"access_token": access_token,
|
@@ -117,20 +143,26 @@ async def refresh_token(refresh_token: str):
|
|
117
143
|
if payload.get("type") != "refresh":
|
118
144
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
|
119
145
|
|
120
|
-
|
121
|
-
user = await User.filter(
|
146
|
+
user_id = payload.get("sub")
|
147
|
+
user = await User.filter(id=user_id).first()
|
122
148
|
if not user:
|
123
149
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
|
124
150
|
|
125
|
-
new_access = create_token(
|
151
|
+
new_access = create_token(claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS)
|
126
152
|
return {"access_token": new_access, "token_type": "bearer"}
|
127
153
|
|
128
154
|
|
129
155
|
@router.get("/me")
|
130
|
-
async def me(
|
156
|
+
async def me(user: User = Depends(get_current_user)):
|
131
157
|
"""Return the currently authenticated user."""
|
132
158
|
return {
|
133
|
-
"
|
134
|
-
"
|
135
|
-
"
|
159
|
+
"email": user.email,
|
160
|
+
"username": user.username,
|
161
|
+
"is_admin": user.is_admin,
|
162
|
+
"is_staff": user.is_staff,
|
136
163
|
}
|
164
|
+
|
165
|
+
|
166
|
+
@router.get("/introspect")
|
167
|
+
async def introspect(token: Dict = Depends(get_token)):
|
168
|
+
return token
|
ohmyapi/cli.py
CHANGED
@@ -9,7 +9,9 @@ from ohmyapi.core import scaffolding, runtime
|
|
9
9
|
from pathlib import Path
|
10
10
|
|
11
11
|
app = typer.Typer(help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM.")
|
12
|
-
banner = """OhMyAPI Shell | Project: {project_name}
|
12
|
+
banner = """OhMyAPI Shell | Project: {project_name}
|
13
|
+
Find your loaded project singleton via identifier: `p`
|
14
|
+
"""
|
13
15
|
|
14
16
|
|
15
17
|
@app.command()
|
@@ -46,16 +48,14 @@ def shell(root: str = "."):
|
|
46
48
|
try:
|
47
49
|
from IPython import start_ipython
|
48
50
|
shell_vars = {
|
49
|
-
"
|
50
|
-
"project": Path(project_path).resolve(),
|
51
|
+
"p": project,
|
51
52
|
}
|
52
53
|
from traitlets.config.loader import Config
|
53
54
|
c = Config()
|
54
55
|
c.TerminalIPythonApp.display_banner = True
|
55
|
-
c.TerminalInteractiveShell.
|
56
|
+
c.TerminalInteractiveShell.banner2 = banner.format(**{
|
56
57
|
"project_name": f"{f'{project.settings.PROJECT_NAME} ' if getattr(project.settings, 'PROJECT_NAME', '') else ''}[{Path(project_path).resolve()}]",
|
57
58
|
})
|
58
|
-
c.TerminalInteractiveShell.banner2 = " "
|
59
59
|
start_ipython(argv=[], user_ns=shell_vars, config=c)
|
60
60
|
except ImportError:
|
61
61
|
typer.echo("IPython is not installed. Falling back to built-in Python shell.")
|
@@ -101,9 +101,10 @@ def createsuperuser(root: str = "."):
|
|
101
101
|
|
102
102
|
import asyncio
|
103
103
|
import ohmyapi_auth
|
104
|
+
email = input("E-Mail: ")
|
104
105
|
username = input("Username: ")
|
105
106
|
password = getpass("Password: ")
|
106
|
-
user = ohmyapi_auth.models.User(username=username, is_staff=True, is_admin=True)
|
107
|
+
user = ohmyapi_auth.models.User(email=email, username=username, is_staff=True, is_admin=True)
|
107
108
|
user.set_password(password)
|
108
109
|
asyncio.run(project.init_orm())
|
109
110
|
asyncio.run(user.save())
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: ohmyapi
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.9
|
4
4
|
Summary: A Django-like but async web-framework based on FastAPI and TortoiseORM.
|
5
5
|
License-Expression: MIT
|
6
6
|
Keywords: fastapi,tortoise,orm,async,web-framework
|
@@ -32,26 +32,32 @@ Description-Content-Type: text/markdown
|
|
32
32
|
|
33
33
|
# OhMyAPI
|
34
34
|
|
35
|
-
> Think:
|
35
|
+
> Think: Django RestFramework, but less clunky and 100% async.
|
36
36
|
|
37
37
|
OhMyAPI is a Django-flavored web-application scaffolding framework and management layer.
|
38
38
|
Built around FastAPI and TortoiseORM, it is 100% async.
|
39
39
|
|
40
|
-
It is ***blazingly fast***, ***fun
|
40
|
+
It is ***blazingly fast***, ***fun to use*** and comes with ***batteries included***!
|
41
41
|
|
42
42
|
**Features**
|
43
43
|
|
44
44
|
- Django-like project-layout and -structure
|
45
|
-
- Django-like
|
45
|
+
- Django-like project-level settings.py
|
46
46
|
- Django-like models via TortoiseORM
|
47
47
|
- Django-like `Model.Meta` class for model configuration
|
48
48
|
- Easily convert your query results to `pydantic` models via `Model.Schema`
|
49
|
-
- Django-like migrations (makemigrations & migrate) via Aerich
|
49
|
+
- Django-like migrations (`makemigrations` & `migrate`) via Aerich
|
50
50
|
- Django-like CLI tooling (`startproject`, `startapp`, `shell`, `serve`, etc)
|
51
51
|
- Various optional builtin apps you can hook into your project
|
52
52
|
- Highly configurable and customizable
|
53
53
|
- 100% async
|
54
54
|
|
55
|
+
OhMyAPI aims to:
|
56
|
+
|
57
|
+
- combine FastAPI, TortoiseORM and Aerich migrations into a high-productivity web-application framework
|
58
|
+
- tying everything neatly together into a project structure consisting of apps with models and a router
|
59
|
+
- while ***AVOIDING*** to introduce any additional abstractions ontop of Tortoise's model-system or FastAPI's routing
|
60
|
+
|
55
61
|
---
|
56
62
|
|
57
63
|
## Getting started
|
@@ -114,7 +120,7 @@ from ohmyapi.db import Model, field
|
|
114
120
|
|
115
121
|
|
116
122
|
class Tournament(Model):
|
117
|
-
id = field.
|
123
|
+
id = field.data.UUIDField(primary_key=True)
|
118
124
|
name = field.TextField()
|
119
125
|
created = field.DatetimeField(auto_now_add=True)
|
120
126
|
|
@@ -123,10 +129,10 @@ class Tournament(Model):
|
|
123
129
|
|
124
130
|
|
125
131
|
class Event(Model):
|
126
|
-
id = field.
|
132
|
+
id = field.data.UUIDField(primary_key=True)
|
127
133
|
name = field.TextField()
|
128
134
|
tournament = field.ForeignKeyField('tournament.Tournament', related_name='events')
|
129
|
-
participants = field.ManyToManyField('
|
135
|
+
participants = field.ManyToManyField('tournament.Team', related_name='events', through='event_team')
|
130
136
|
modified = field.DatetimeField(auto_now=True)
|
131
137
|
prize = field.DecimalField(max_digits=10, decimal_places=2, null=True)
|
132
138
|
|
@@ -135,7 +141,7 @@ class Event(Model):
|
|
135
141
|
|
136
142
|
|
137
143
|
class Team(Model):
|
138
|
-
id = field.
|
144
|
+
id = field.data.UUIDField(primary_key=True)
|
139
145
|
name = field.TextField()
|
140
146
|
|
141
147
|
def __str__(self):
|
@@ -162,7 +168,7 @@ async def list():
|
|
162
168
|
|
163
169
|
|
164
170
|
@router.get("/:id")
|
165
|
-
async def get(id:
|
171
|
+
async def get(id: str):
|
166
172
|
try:
|
167
173
|
queryset = Tournament.get(pk=id)
|
168
174
|
return await Tournament.Schema.one(queryset)
|
@@ -209,14 +215,6 @@ Run your project:
|
|
209
215
|
ohmyapi serve
|
210
216
|
```
|
211
217
|
|
212
|
-
## Shell
|
213
|
-
|
214
|
-
Similar to Django, you can attach to an interactive shell with your project already loaded inside.
|
215
|
-
|
216
|
-
```
|
217
|
-
ohmyapi shell
|
218
|
-
```
|
219
|
-
|
220
218
|
## Authentication
|
221
219
|
|
222
220
|
A builtin auth app is available.
|
@@ -289,7 +287,7 @@ async def list(user: auth.User = Depends(permissions.require_authenticated)):
|
|
289
287
|
|
290
288
|
### Model-Level Permissions
|
291
289
|
|
292
|
-
Use Tortoise's `Manager` to implement model-
|
290
|
+
Use Tortoise's `Manager` to implement model-level permissions.
|
293
291
|
|
294
292
|
```python
|
295
293
|
from ohmyapi.db import Manager
|
@@ -297,7 +295,7 @@ from typing import Callable
|
|
297
295
|
|
298
296
|
|
299
297
|
class TeamManager(Manager):
|
300
|
-
async def for_user(self, user):
|
298
|
+
async def for_user(self, user: ohmyapi_auth.models.User):
|
301
299
|
return await self.filter(members=user).all()
|
302
300
|
|
303
301
|
|
@@ -308,3 +306,40 @@ class Team(Model):
|
|
308
306
|
manager = TeamManager()
|
309
307
|
```
|
310
308
|
|
309
|
+
## Shell
|
310
|
+
|
311
|
+
Similar to Django, you can attach to an interactive shell with your project already loaded inside.
|
312
|
+
|
313
|
+
```
|
314
|
+
ohmyapi shell
|
315
|
+
|
316
|
+
Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813]
|
317
|
+
Type 'copyright', 'credits' or 'license' for more information
|
318
|
+
IPython 9.5.0 -- An enhanced Interactive Python. Type '?' for help.
|
319
|
+
|
320
|
+
OhMyAPI Shell | Project: {{ project_name }} [{{ project_path }}]
|
321
|
+
Find your loaded project singleton via identifier: `p`
|
322
|
+
```
|
323
|
+
|
324
|
+
```python
|
325
|
+
In [1]: p
|
326
|
+
Out[1]: <ohmyapi.core.runtime.Project at 0xdeadbeefc0febabe>
|
327
|
+
|
328
|
+
In [2]: p.apps
|
329
|
+
Out[2]:
|
330
|
+
{'ohmyapi_auth': App: ohmyapi_auth
|
331
|
+
Models:
|
332
|
+
- Group
|
333
|
+
- User
|
334
|
+
Routes:
|
335
|
+
- APIRoute(path='/auth/login', name='login', methods=['POST'])
|
336
|
+
- APIRoute(path='/auth/refresh', name='refresh_token', methods=['POST'])
|
337
|
+
- APIRoute(path='/auth/me', name='me', methods=['GET'])
|
338
|
+
- APIRoute(path='/auth/introspect', name='introspect', methods=['GET'])}
|
339
|
+
|
340
|
+
In [3]: from tournament.models import Tournament
|
341
|
+
Out[3]:
|
342
|
+
|
343
|
+
```
|
344
|
+
|
345
|
+
|
@@ -1,10 +1,10 @@
|
|
1
1
|
ohmyapi/__init__.py,sha256=UmLNQImTbKvHEgwQB2Wsyl6fq88X92imL9QZYJpQX4I,18
|
2
2
|
ohmyapi/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
|
3
3
|
ohmyapi/builtin/auth/__init__.py,sha256=TY1RKgwWmJ6FKz_v4J3m0Ang69qSmtVDLe4rqjLk4-E,69
|
4
|
-
ohmyapi/builtin/auth/models.py,sha256=
|
5
|
-
ohmyapi/builtin/auth/permissions.py,sha256=
|
6
|
-
ohmyapi/builtin/auth/routes.py,sha256=
|
7
|
-
ohmyapi/cli.py,sha256=
|
4
|
+
ohmyapi/builtin/auth/models.py,sha256=Xsxn9m5RTgY2a0PPfW3wTj77ocuuISytdl4ec_TR_kw,1524
|
5
|
+
ohmyapi/builtin/auth/permissions.py,sha256=NKljLhgEHcEIlzpWgqFyz-1PeCT2u0Vqkja4xy-Zj68,126
|
6
|
+
ohmyapi/builtin/auth/routes.py,sha256=DxlVzHSdMIbKMnWXMMGj_M-jUMFLHTt8avzBviM7Ia0,5625
|
7
|
+
ohmyapi/cli.py,sha256=ZJVBRpSS297y00H4zffYQzkeIIVmVpGS4BYGpxT1FPo,3430
|
8
8
|
ohmyapi/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
9
9
|
ohmyapi/core/runtime.py,sha256=l5zffc4VTwQOo7G9mfiYHsPGapMONRX_jtO_z9QaKHU,8577
|
10
10
|
ohmyapi/core/scaffolding.py,sha256=iMymscokJ-YqzB0ZTC-gcc2T71o73577j9tnb2x8lH8,2572
|
@@ -19,7 +19,7 @@ ohmyapi/db/exceptions.py,sha256=I7AubrdqQF_UvAvzKqz2ve08-BkXHzEWXnwG300StHE,35
|
|
19
19
|
ohmyapi/db/model/__init__.py,sha256=k3StTNuKatpwZo_Z5JBFa-927eJrzibFE8U4SA82asc,32
|
20
20
|
ohmyapi/db/model/model.py,sha256=BajFtLlQ1s0mZ2hj-_JNQhLQmxuVe-Lw2LuW5t2C7Rw,1579
|
21
21
|
ohmyapi/router.py,sha256=hutccsrP9RT8W5O6uBDhOJehwqrkRoPzaUI5zoHPh9A,55
|
22
|
-
ohmyapi-0.1.
|
23
|
-
ohmyapi-0.1.
|
24
|
-
ohmyapi-0.1.
|
25
|
-
ohmyapi-0.1.
|
22
|
+
ohmyapi-0.1.9.dist-info/METADATA,sha256=9eAyhG-3i6ImpmWhEQaXlfGle9NGvysMCsSqbJ4e3Jc,8065
|
23
|
+
ohmyapi-0.1.9.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
|
24
|
+
ohmyapi-0.1.9.dist-info/entry_points.txt,sha256=wb3lw8-meAlpiv1mqcQ3m25ukL7djagU_w89GkrC37k,43
|
25
|
+
ohmyapi-0.1.9.dist-info/RECORD,,
|
File without changes
|
File without changes
|