ohmyapi 0.1.7__tar.gz → 0.1.9__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.
Files changed (24) hide show
  1. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/PKG-INFO +55 -20
  2. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/README.md +54 -19
  3. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/pyproject.toml +1 -1
  4. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/builtin/auth/models.py +2 -2
  5. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/builtin/auth/permissions.py +1 -0
  6. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/builtin/auth/routes.py +46 -14
  7. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/cli.py +7 -6
  8. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/__init__.py +0 -0
  9. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/__main__.py +0 -0
  10. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/builtin/auth/__init__.py +0 -0
  11. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/__init__.py +0 -0
  12. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/runtime.py +0 -0
  13. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/scaffolding.py +0 -0
  14. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/templates/app/__init__.py.j2 +0 -0
  15. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/templates/app/models.py.j2 +0 -0
  16. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/templates/app/routes.py.j2 +0 -0
  17. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/templates/project/README.md.j2 +0 -0
  18. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/templates/project/pyproject.toml.j2 +0 -0
  19. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/core/templates/project/settings.py.j2 +0 -0
  20. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/db/__init__.py +0 -0
  21. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/db/exceptions.py +0 -0
  22. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/db/model/__init__.py +0 -0
  23. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/db/model/model.py +0 -0
  24. {ohmyapi-0.1.7 → ohmyapi-0.1.9}/src/ohmyapi/router.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ohmyapi
3
- Version: 0.1.7
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: Micro-Django, but API-first, less clunky and 100% async.
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*** to use and comes with ***batteries included***!
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 prject-level settings.py
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.IntField(primary_key=True)
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.IntField(primary_key=True)
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('torunament.Team', related_name='events', through='event_team')
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.IntField(primary_key=True)
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: int):
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-layer permissions.
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,25 +1,31 @@
1
1
  # OhMyAPI
2
2
 
3
- > Think: Micro-Django, but API-first, less clunky and 100% async.
3
+ > Think: Django RestFramework, but less clunky and 100% async.
4
4
 
5
5
  OhMyAPI is a Django-flavored web-application scaffolding framework and management layer.
6
6
  Built around FastAPI and TortoiseORM, it is 100% async.
7
7
 
8
- It is ***blazingly fast***, ***fun*** to use and comes with ***batteries included***!
8
+ It is ***blazingly fast***, ***fun to use*** and comes with ***batteries included***!
9
9
 
10
10
  **Features**
11
11
 
12
12
  - Django-like project-layout and -structure
13
- - Django-like prject-level settings.py
13
+ - Django-like project-level settings.py
14
14
  - Django-like models via TortoiseORM
15
15
  - Django-like `Model.Meta` class for model configuration
16
16
  - Easily convert your query results to `pydantic` models via `Model.Schema`
17
- - Django-like migrations (makemigrations & migrate) via Aerich
17
+ - Django-like migrations (`makemigrations` & `migrate`) via Aerich
18
18
  - Django-like CLI tooling (`startproject`, `startapp`, `shell`, `serve`, etc)
19
19
  - Various optional builtin apps you can hook into your project
20
20
  - Highly configurable and customizable
21
21
  - 100% async
22
22
 
23
+ OhMyAPI aims to:
24
+
25
+ - combine FastAPI, TortoiseORM and Aerich migrations into a high-productivity web-application framework
26
+ - tying everything neatly together into a project structure consisting of apps with models and a router
27
+ - while ***AVOIDING*** to introduce any additional abstractions ontop of Tortoise's model-system or FastAPI's routing
28
+
23
29
  ---
24
30
 
25
31
  ## Getting started
@@ -82,7 +88,7 @@ from ohmyapi.db import Model, field
82
88
 
83
89
 
84
90
  class Tournament(Model):
85
- id = field.IntField(primary_key=True)
91
+ id = field.data.UUIDField(primary_key=True)
86
92
  name = field.TextField()
87
93
  created = field.DatetimeField(auto_now_add=True)
88
94
 
@@ -91,10 +97,10 @@ class Tournament(Model):
91
97
 
92
98
 
93
99
  class Event(Model):
94
- id = field.IntField(primary_key=True)
100
+ id = field.data.UUIDField(primary_key=True)
95
101
  name = field.TextField()
96
102
  tournament = field.ForeignKeyField('tournament.Tournament', related_name='events')
97
- participants = field.ManyToManyField('torunament.Team', related_name='events', through='event_team')
103
+ participants = field.ManyToManyField('tournament.Team', related_name='events', through='event_team')
98
104
  modified = field.DatetimeField(auto_now=True)
99
105
  prize = field.DecimalField(max_digits=10, decimal_places=2, null=True)
100
106
 
@@ -103,7 +109,7 @@ class Event(Model):
103
109
 
104
110
 
105
111
  class Team(Model):
106
- id = field.IntField(primary_key=True)
112
+ id = field.data.UUIDField(primary_key=True)
107
113
  name = field.TextField()
108
114
 
109
115
  def __str__(self):
@@ -130,7 +136,7 @@ async def list():
130
136
 
131
137
 
132
138
  @router.get("/:id")
133
- async def get(id: int):
139
+ async def get(id: str):
134
140
  try:
135
141
  queryset = Tournament.get(pk=id)
136
142
  return await Tournament.Schema.one(queryset)
@@ -177,14 +183,6 @@ Run your project:
177
183
  ohmyapi serve
178
184
  ```
179
185
 
180
- ## Shell
181
-
182
- Similar to Django, you can attach to an interactive shell with your project already loaded inside.
183
-
184
- ```
185
- ohmyapi shell
186
- ```
187
-
188
186
  ## Authentication
189
187
 
190
188
  A builtin auth app is available.
@@ -257,7 +255,7 @@ async def list(user: auth.User = Depends(permissions.require_authenticated)):
257
255
 
258
256
  ### Model-Level Permissions
259
257
 
260
- Use Tortoise's `Manager` to implement model-layer permissions.
258
+ Use Tortoise's `Manager` to implement model-level permissions.
261
259
 
262
260
  ```python
263
261
  from ohmyapi.db import Manager
@@ -265,7 +263,7 @@ from typing import Callable
265
263
 
266
264
 
267
265
  class TeamManager(Manager):
268
- async def for_user(self, user):
266
+ async def for_user(self, user: ohmyapi_auth.models.User):
269
267
  return await self.filter(members=user).all()
270
268
 
271
269
 
@@ -275,3 +273,40 @@ class Team(Model):
275
273
  class Meta:
276
274
  manager = TeamManager()
277
275
  ```
276
+
277
+ ## Shell
278
+
279
+ Similar to Django, you can attach to an interactive shell with your project already loaded inside.
280
+
281
+ ```
282
+ ohmyapi shell
283
+
284
+ Python 3.13.7 (main, Aug 15 2025, 12:34:02) [GCC 15.2.1 20250813]
285
+ Type 'copyright', 'credits' or 'license' for more information
286
+ IPython 9.5.0 -- An enhanced Interactive Python. Type '?' for help.
287
+
288
+ OhMyAPI Shell | Project: {{ project_name }} [{{ project_path }}]
289
+ Find your loaded project singleton via identifier: `p`
290
+ ```
291
+
292
+ ```python
293
+ In [1]: p
294
+ Out[1]: <ohmyapi.core.runtime.Project at 0xdeadbeefc0febabe>
295
+
296
+ In [2]: p.apps
297
+ Out[2]:
298
+ {'ohmyapi_auth': App: ohmyapi_auth
299
+ Models:
300
+ - Group
301
+ - User
302
+ Routes:
303
+ - APIRoute(path='/auth/login', name='login', methods=['POST'])
304
+ - APIRoute(path='/auth/refresh', name='refresh_token', methods=['POST'])
305
+ - APIRoute(path='/auth/me', name='me', methods=['GET'])
306
+ - APIRoute(path='/auth/introspect', name='introspect', methods=['GET'])}
307
+
308
+ In [3]: from tournament.models import Tournament
309
+ Out[3]:
310
+
311
+ ```
312
+
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ohmyapi"
3
- version = "0.1.7"
3
+ version = "0.1.9"
4
4
  description = "A Django-like but async web-framework based on FastAPI and TortoiseORM."
5
5
  license = "MIT"
6
6
  keywords = ["fastapi", "tortoise", "orm", "async", "web-framework"]
@@ -7,12 +7,12 @@ pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
7
7
 
8
8
 
9
9
  class Group(Model):
10
- id = field.IntField(pk=True)
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.IntField(pk=True)
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)
@@ -1,4 +1,5 @@
1
1
  from .routes import (
2
+ get_token,
2
3
  get_current_user,
3
4
  require_authenticated,
4
5
  require_admin,
@@ -1,12 +1,13 @@
1
1
  import time
2
- from typing import Dict
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
- username = payload.get("sub")
47
- if username is None:
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(username=username).first()
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({"sub": user.username, "type": "access"}, ACCESS_TOKEN_EXPIRE_SECONDS)
104
- refresh_token = create_token({"sub": user.username, "type": "refresh"}, REFRESH_TOKEN_EXPIRE_SECONDS)
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
- username = payload.get("sub")
121
- user = await User.filter(username=username).first()
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({"sub": user.username, "type": "access"}, ACCESS_TOKEN_EXPIRE_SECONDS)
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(current_user: User = Depends(get_current_user)):
156
+ async def me(user: User = Depends(get_current_user)):
131
157
  """Return the currently authenticated user."""
132
158
  return {
133
- "username": current_user.username,
134
- "is_admin": current_user.is_admin,
135
- "is_staff": current_user.is_staff,
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
@@ -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
- "settings": project.settings,
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.banner1 = banner.format(**{
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())
File without changes
File without changes
File without changes