ohmyapi 0.1.21__tar.gz → 0.1.23__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 (28) hide show
  1. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/PKG-INFO +67 -24
  2. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/README.md +66 -23
  3. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/pyproject.toml +1 -1
  4. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/builtin/auth/routes.py +69 -42
  5. ohmyapi-0.1.23/src/ohmyapi/builtin/demo/__init__.py +2 -0
  6. ohmyapi-0.1.23/src/ohmyapi/builtin/demo/models.py +50 -0
  7. ohmyapi-0.1.23/src/ohmyapi/builtin/demo/routes.py +54 -0
  8. ohmyapi-0.1.23/src/ohmyapi/core/templates/app/routes.py.j2 +41 -0
  9. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/router.py +1 -0
  10. ohmyapi-0.1.21/src/ohmyapi/core/templates/app/routes.py.j2 +0 -17
  11. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/__init__.py +0 -0
  12. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/__main__.py +0 -0
  13. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/builtin/auth/__init__.py +0 -0
  14. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/builtin/auth/models.py +0 -0
  15. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/builtin/auth/permissions.py +0 -0
  16. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/cli.py +0 -0
  17. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/core/__init__.py +0 -0
  18. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/core/runtime.py +0 -0
  19. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/core/scaffolding.py +0 -0
  20. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/core/templates/app/__init__.py.j2 +0 -0
  21. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/core/templates/app/models.py.j2 +0 -0
  22. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/core/templates/project/README.md.j2 +0 -0
  23. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/core/templates/project/pyproject.toml.j2 +0 -0
  24. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/core/templates/project/settings.py.j2 +0 -0
  25. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/db/__init__.py +0 -0
  26. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/db/exceptions.py +0 -0
  27. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/db/model/__init__.py +0 -0
  28. {ohmyapi-0.1.21 → ohmyapi-0.1.23}/src/ohmyapi/db/model/model.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ohmyapi
3
- Version: 0.1.21
3
+ Version: 0.1.23
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
@@ -157,7 +157,7 @@ class Team(Model):
157
157
  Next, create your endpoints in `tournament/routes.py`:
158
158
 
159
159
  ```python
160
- from ohmyapi.router import APIRouter, HTTPException
160
+ from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
161
161
  from ohmyapi.db.exceptions import DoesNotExist
162
162
 
163
163
  from .models import Tournament
@@ -167,20 +167,25 @@ from .models import Tournament
167
167
  # Tags improve the UX of the OpenAPI docs at /docs.
168
168
  router = APIRouter(prefix="/tournament", tags=['Tournament'])
169
169
 
170
-
171
170
  @router.get("/")
172
171
  async def list():
173
172
  queryset = Tournament.all()
174
173
  return await Tournament.Schema.model.from_queryset(queryset)
175
174
 
176
175
 
176
+ @router.post("/", status_code=HTTPStatus.CREATED)
177
+ async def post(tournament: Tournament.Schema.readonly):
178
+ queryset = Tournament.create(**payload.model_dump())
179
+ return await Tournament.Schema.model.from_queryset(queryset)
180
+
181
+
177
182
  @router.get("/:id")
178
183
  async def get(id: str):
179
184
  try:
180
- tournament = await Tournament.get(pk=id)
185
+ queryset = Tournament.get(id=id)
181
186
  return await Tournament.Schema.model.from_queryset_single(tournament)
182
187
  except DoesNotExist:
183
- raise HTTPException(status_code=404, detail="item not found")
188
+ raise HTTPException(status_code=404, detail="not found")
184
189
 
185
190
  ...
186
191
  ```
@@ -350,28 +355,66 @@ Find your loaded project singleton via identifier: `p`
350
355
 
351
356
  ```python
352
357
  In [1]: p
353
- Out[1]: <ohmyapi.core.runtime.Project at 0xdeadbeefc0febabe>
358
+ Out[1]: <ohmyapi.core.runtime.Project at 0x7f00c43dbcb0>
354
359
 
355
360
  In [2]: p.apps
356
361
  Out[2]:
357
- {'ohmyapi_auth': App: ohmyapi_auth
358
- Models:
359
- - Group
360
- - User
361
- Routes:
362
- - APIRoute(path='/auth/login', name='login', methods=['POST'])
363
- - APIRoute(path='/auth/refresh', name='refresh_token', methods=['POST'])
364
- - APIRoute(path='/auth/introspect', name='introspect', methods=['GET'])
365
- - APIRoute(path='/auth/me', name='me', methods=['GET']),
366
- 'tournament': App: tournament
367
- Models:
368
- - Tournament
369
- - Event
370
- - Team
371
- Routes:
372
- - APIRoute(path='/tournament/', name='list', methods=['GET'])}
373
-
374
- In [3]: from tournament.models import Tournament
362
+ {'ohmyapi_auth': {
363
+ "models": [
364
+ "Group",
365
+ "User"
366
+ ],
367
+ "routes": [
368
+ {
369
+ "path": "/auth/login",
370
+ "name": "login",
371
+ "methods": [
372
+ "POST"
373
+ ],
374
+ "endpoint": "login",
375
+ "response_model": null,
376
+ "tags": [
377
+ "auth"
378
+ ]
379
+ },
380
+ {
381
+ "path": "/auth/refresh",
382
+ "name": "refresh_token",
383
+ "methods": [
384
+ "POST"
385
+ ],
386
+ "endpoint": "refresh_token",
387
+ "response_model": null,
388
+ "tags": [
389
+ "auth"
390
+ ]
391
+ },
392
+ {
393
+ "path": "/auth/introspect",
394
+ "name": "introspect",
395
+ "methods": [
396
+ "GET"
397
+ ],
398
+ "endpoint": "introspect",
399
+ "response_model": null,
400
+ "tags": [
401
+ "auth"
402
+ ]
403
+ },
404
+ {
405
+ "path": "/auth/me",
406
+ "name": "me",
407
+ "methods": [
408
+ "GET"
409
+ ],
410
+ "endpoint": "me",
411
+ "response_model": null,
412
+ "tags": [
413
+ "auth"
414
+ ]
415
+ }
416
+ ]
417
+ }}
375
418
  ```
376
419
 
377
420
 
@@ -125,7 +125,7 @@ class Team(Model):
125
125
  Next, create your endpoints in `tournament/routes.py`:
126
126
 
127
127
  ```python
128
- from ohmyapi.router import APIRouter, HTTPException
128
+ from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
129
129
  from ohmyapi.db.exceptions import DoesNotExist
130
130
 
131
131
  from .models import Tournament
@@ -135,20 +135,25 @@ from .models import Tournament
135
135
  # Tags improve the UX of the OpenAPI docs at /docs.
136
136
  router = APIRouter(prefix="/tournament", tags=['Tournament'])
137
137
 
138
-
139
138
  @router.get("/")
140
139
  async def list():
141
140
  queryset = Tournament.all()
142
141
  return await Tournament.Schema.model.from_queryset(queryset)
143
142
 
144
143
 
144
+ @router.post("/", status_code=HTTPStatus.CREATED)
145
+ async def post(tournament: Tournament.Schema.readonly):
146
+ queryset = Tournament.create(**payload.model_dump())
147
+ return await Tournament.Schema.model.from_queryset(queryset)
148
+
149
+
145
150
  @router.get("/:id")
146
151
  async def get(id: str):
147
152
  try:
148
- tournament = await Tournament.get(pk=id)
153
+ queryset = Tournament.get(id=id)
149
154
  return await Tournament.Schema.model.from_queryset_single(tournament)
150
155
  except DoesNotExist:
151
- raise HTTPException(status_code=404, detail="item not found")
156
+ raise HTTPException(status_code=404, detail="not found")
152
157
 
153
158
  ...
154
159
  ```
@@ -318,27 +323,65 @@ Find your loaded project singleton via identifier: `p`
318
323
 
319
324
  ```python
320
325
  In [1]: p
321
- Out[1]: <ohmyapi.core.runtime.Project at 0xdeadbeefc0febabe>
326
+ Out[1]: <ohmyapi.core.runtime.Project at 0x7f00c43dbcb0>
322
327
 
323
328
  In [2]: p.apps
324
329
  Out[2]:
325
- {'ohmyapi_auth': App: ohmyapi_auth
326
- Models:
327
- - Group
328
- - User
329
- Routes:
330
- - APIRoute(path='/auth/login', name='login', methods=['POST'])
331
- - APIRoute(path='/auth/refresh', name='refresh_token', methods=['POST'])
332
- - APIRoute(path='/auth/introspect', name='introspect', methods=['GET'])
333
- - APIRoute(path='/auth/me', name='me', methods=['GET']),
334
- 'tournament': App: tournament
335
- Models:
336
- - Tournament
337
- - Event
338
- - Team
339
- Routes:
340
- - APIRoute(path='/tournament/', name='list', methods=['GET'])}
341
-
342
- In [3]: from tournament.models import Tournament
330
+ {'ohmyapi_auth': {
331
+ "models": [
332
+ "Group",
333
+ "User"
334
+ ],
335
+ "routes": [
336
+ {
337
+ "path": "/auth/login",
338
+ "name": "login",
339
+ "methods": [
340
+ "POST"
341
+ ],
342
+ "endpoint": "login",
343
+ "response_model": null,
344
+ "tags": [
345
+ "auth"
346
+ ]
347
+ },
348
+ {
349
+ "path": "/auth/refresh",
350
+ "name": "refresh_token",
351
+ "methods": [
352
+ "POST"
353
+ ],
354
+ "endpoint": "refresh_token",
355
+ "response_model": null,
356
+ "tags": [
357
+ "auth"
358
+ ]
359
+ },
360
+ {
361
+ "path": "/auth/introspect",
362
+ "name": "introspect",
363
+ "methods": [
364
+ "GET"
365
+ ],
366
+ "endpoint": "introspect",
367
+ "response_model": null,
368
+ "tags": [
369
+ "auth"
370
+ ]
371
+ },
372
+ {
373
+ "path": "/auth/me",
374
+ "name": "me",
375
+ "methods": [
376
+ "GET"
377
+ ],
378
+ "endpoint": "me",
379
+ "response_model": null,
380
+ "tags": [
381
+ "auth"
382
+ ]
383
+ }
384
+ ]
385
+ }}
343
386
  ```
344
387
 
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "ohmyapi"
3
- version = "0.1.21"
3
+ version = "0.1.23"
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"]
@@ -26,9 +26,64 @@ REFRESH_TOKEN_EXPIRE_SECONDS = getattr(
26
26
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
27
27
 
28
28
 
29
- def create_token(data: dict, expires_in: int) -> str:
30
- to_encode = data.copy()
31
- to_encode.update({"exp": int(time.time()) + expires_in})
29
+ class ClaimsUser(BaseModel):
30
+ username: str
31
+ email: str
32
+ is_admin: bool
33
+ is_staff: bool
34
+
35
+
36
+ class Claims(BaseModel):
37
+ type: str
38
+ sub: str
39
+ user: ClaimsUser
40
+ roles: List[str]
41
+ exp: str
42
+
43
+
44
+ class AccessToken(BaseModel):
45
+ token_type: str
46
+ access_token: str
47
+
48
+
49
+ class RefreshToken(AccessToken):
50
+ refresh_token: str
51
+
52
+
53
+ class LoginRequest(BaseModel):
54
+ username: str
55
+ password: str
56
+
57
+
58
+ class TokenType(str, Enum):
59
+ """
60
+ Helper for indicating the token type when generating claims.
61
+ """
62
+
63
+ access = "access"
64
+ refresh = "refresh"
65
+
66
+
67
+ def claims(
68
+ token_type: TokenType, user: User, groups: List[Group] = []
69
+ ) -> Claims:
70
+ return Claims(
71
+ type=token_type,
72
+ sub=str(user.id),
73
+ user=ClaimsUser(
74
+ username=user.username,
75
+ email=user.email,
76
+ is_admin=user.is_admin,
77
+ is_staff=user.is_staff,
78
+ ),
79
+ roles=[g.name for g in groups],
80
+ exp="",
81
+ )
82
+
83
+
84
+ def create_token(claims: Claims, expires_in: int) -> str:
85
+ to_encode = claims.model_dump()
86
+ to_encode['exp'] = int(time.time()) + expires_in
32
87
  token = jwt.encode(to_encode, JWT_SECRET, algorithm=JWT_ALGORITHM)
33
88
  if isinstance(token, bytes):
34
89
  token = token.decode("utf-8")
@@ -48,29 +103,6 @@ def decode_token(token: str) -> Dict:
48
103
  )
49
104
 
50
105
 
51
- class TokenType(str, Enum):
52
- """
53
- Helper for indicating the token type when generating claims.
54
- """
55
-
56
- access = "access"
57
- refresh = "refresh"
58
-
59
-
60
- def claims(
61
- token_type: TokenType, user: User, groups: List[Group] = []
62
- ) -> Dict[str, Any]:
63
- return {
64
- "type": token_type,
65
- "sub": str(user.id),
66
- "user": {
67
- "username": user.username,
68
- "email": user.email,
69
- },
70
- "roles": [g.name for g in groups],
71
- }
72
-
73
-
74
106
  async def get_token(token: str = Depends(oauth2_scheme)) -> Dict:
75
107
  """Dependency: token introspection"""
76
108
  payload = decode_token(token)
@@ -127,12 +159,7 @@ async def require_group(
127
159
  return current_user
128
160
 
129
161
 
130
- class LoginRequest(BaseModel):
131
- username: str
132
- password: str
133
-
134
-
135
- @router.post("/login")
162
+ @router.post("/login", response_model=RefreshToken)
136
163
  async def login(form_data: LoginRequest = Body(...)):
137
164
  """Login with username & password, returns access and refresh tokens."""
138
165
  user = await User.authenticate(form_data.username, form_data.password)
@@ -148,14 +175,14 @@ async def login(form_data: LoginRequest = Body(...)):
148
175
  claims(TokenType.refresh, user), REFRESH_TOKEN_EXPIRE_SECONDS
149
176
  )
150
177
 
151
- return {
152
- "access_token": access_token,
153
- "refresh_token": refresh_token,
154
- "token_type": "bearer",
155
- }
178
+ return RefreshToken(
179
+ token_type="bearer",
180
+ access_token=access_token,
181
+ refresh_token=refresh_token,
182
+ )
156
183
 
157
184
 
158
- @router.post("/refresh")
185
+ @router.post("/refresh", response_model=AccessToken)
159
186
  async def refresh_token(refresh_token: str):
160
187
  """Exchange refresh token for new access token."""
161
188
  payload = decode_token(refresh_token)
@@ -174,15 +201,15 @@ async def refresh_token(refresh_token: str):
174
201
  new_access = create_token(
175
202
  claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS
176
203
  )
177
- return {"access_token": new_access, "token_type": "bearer"}
204
+ return AccessToken(token_type="bearer", access_token=access_token)
178
205
 
179
206
 
180
- @router.get("/introspect")
207
+ @router.get("/introspect", response_model=Dict[str, Any])
181
208
  async def introspect(token: Dict = Depends(get_token)):
182
209
  return token
183
210
 
184
211
 
185
- @router.get("/me")
212
+ @router.get("/me", response_model=User.Schema.model)
186
213
  async def me(user: User = Depends(get_current_user)):
187
214
  """Return the currently authenticated user."""
188
- return User.Schema.one.from_orm(user)
215
+ return await User.Schema.model.from_tortoise_orm(user)
@@ -0,0 +1,2 @@
1
+ from . import models
2
+ from . import routes
@@ -0,0 +1,50 @@
1
+ from ohmyapi.db import Model, field
2
+ from ohmyapi_auth.models import User
3
+
4
+ from datetime import datetime
5
+ from decimal import Decimal
6
+ from uuid import UUID
7
+
8
+
9
+ class Team(Model):
10
+ id: UUID = field.data.UUIDField(primary_key=True)
11
+ name: str = field.TextField()
12
+ members: field.ManyToManyRelation[User] = field.ManyToManyField(
13
+ 'ohmyapi_auth.User',
14
+ related_name="tournament_teams",
15
+ through='user_tournament_teams',
16
+ )
17
+
18
+ def __str__(self):
19
+ return self.name
20
+
21
+
22
+ class Tournament(Model):
23
+ id: UUID = field.data.UUIDField(primary_key=True)
24
+ name: str = field.TextField()
25
+ created: datetime = field.DatetimeField(auto_now_add=True)
26
+
27
+ def __str__(self):
28
+ return self.name
29
+
30
+
31
+ class Event(Model):
32
+ id: UUID = field.data.UUIDField(primary_key=True)
33
+ name: str = field.TextField()
34
+ tournament: field.ForeignKeyRelation[Tournament] = field.ForeignKeyField(
35
+ 'ohmyapi_demo.Tournament',
36
+ related_name='events',
37
+ )
38
+ participants: field.ManyToManyRelation[Team] = field.ManyToManyField(
39
+ 'ohmyapi_demo.Team',
40
+ related_name='events',
41
+ through='event_team',
42
+ )
43
+ modified: datetime = field.DatetimeField(auto_now=True)
44
+ prize: Decimal = field.DecimalField(max_digits=10, decimal_places=2, null=True)
45
+
46
+ class Schema:
47
+ exclude = ['tournament_id']
48
+
49
+ def __str__(self):
50
+ return self.name
@@ -0,0 +1,54 @@
1
+ from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
2
+ from ohmyapi.db.exceptions import DoesNotExist
3
+
4
+ from . import models
5
+
6
+ from typing import List
7
+
8
+ # Expose your app's routes via `router = fastapi.APIRouter`.
9
+ # Use prefixes wisely to avoid cross-app namespace-collisions.
10
+ # Tags improve the UX of the OpenAPI docs at /docs.
11
+ router = APIRouter(prefix="/tournemant")
12
+
13
+
14
+ @router.get("/",
15
+ tags=["tournament"],
16
+ response_model=List[models.Tournament.Schema.model])
17
+ async def list():
18
+ """List all tournaments."""
19
+ return await models.Tournament.Schema.model.from_queryset(Tournament.all())
20
+
21
+
22
+ @router.post("/",
23
+ tags=["tournament"],
24
+ status_code=HTTPStatus.CREATED)
25
+ async def post(tournament: models.Tournament.Schema.readonly):
26
+ """Create tournament."""
27
+ return await models.Tournament.Schema.model.from_queryset(models.Tournament.create(**tournament.model_dump()))
28
+
29
+
30
+ @router.get("/{id}",
31
+ tags=["tournament"],
32
+ response_model=models.Tournament.Schema.model)
33
+ async def get(id: str):
34
+ """Get tournament by id."""
35
+ return await models.Tournament.Schema.model.from_queryset(models.Tournament.get(id=id))
36
+
37
+
38
+ @router.put("/{id}",
39
+ tags=["tournament"],
40
+ response_model=models.Tournament.Schema.model,
41
+ status_code=HTTPStatus.ACCEPTED)
42
+ async def put(tournament: models.Tournament.Schema.model):
43
+ """Update tournament."""
44
+ return await models.Tournament.Schema.model.from_queryset(models.Tournament.update(**tournament.model_dump()))
45
+
46
+
47
+ @router.delete("/{id}", tags=["tournament"])
48
+ async def delete(id: str):
49
+ try:
50
+ tournament = await models.Tournament.get(id=id)
51
+ return await tournament.delete()
52
+ except DoesNotExist:
53
+ raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail="not found")
54
+
@@ -0,0 +1,41 @@
1
+ from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
2
+
3
+ from . import models
4
+
5
+ from typing import List
6
+
7
+ # Expose your app's routes via `router = fastapi.APIRouter`.
8
+ # Use prefixes wisely to avoid cross-app namespace-collisions.
9
+ # Tags improve the UX of the OpenAPI docs at /docs.
10
+ router = APIRouter(prefix="/{{ app_name }}", tags=['{{ app_name }}'])
11
+
12
+
13
+
14
+ @router.get("/")
15
+ async def list():
16
+ """List all ..."""
17
+ return []
18
+
19
+
20
+ @router.post("/")
21
+ async def post():
22
+ """Create ..."""
23
+ return HTTPException(status_code=HTTPStatus.CREATED)
24
+
25
+
26
+ @router.get("/{id}")
27
+ async def get(id: str):
28
+ """Get single ..."""
29
+ return {}
30
+
31
+
32
+ @router.put("/{id}")
33
+ async def put(id: str):
34
+ """Update ..."""
35
+ return HTTPException(status_code=HTTPStatus.ACCEPTED)
36
+
37
+
38
+ @router.delete("/{id}")
39
+ async def delete(id: str):
40
+ return HTTPException(status_code=HTTPStatus.ACCEPTED)
41
+
@@ -1 +1,2 @@
1
1
  from fastapi import APIRouter, Depends, HTTPException
2
+ from http import HTTPStatus
@@ -1,17 +0,0 @@
1
- from ohmyapi.router import APIRouter
2
-
3
- from .models import ...
4
-
5
- # Expose your app's routes via `router = fastapi.APIRouter`.
6
- # Use prefixes wisely to avoid cross-app namespace-collisions.
7
- # Tags improve the UX of the OpenAPI docs at /docs.
8
- router = APIRouter(prefix="/{{ app_name }}", tags=['{{ app_name }}'])
9
-
10
-
11
- @router.get("/")
12
- def hello_world():
13
- return {
14
- "project": "{{ project_name }}",
15
- "app": "{{ app_name }}",
16
- }
17
-
File without changes