ohmyapi 0.1.21__py3-none-any.whl → 0.1.23__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.
@@ -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
+
@@ -1,6 +1,8 @@
1
- from ohmyapi.router import APIRouter
1
+ from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
2
2
 
3
- from .models import ...
3
+ from . import models
4
+
5
+ from typing import List
4
6
 
5
7
  # Expose your app's routes via `router = fastapi.APIRouter`.
6
8
  # Use prefixes wisely to avoid cross-app namespace-collisions.
@@ -8,10 +10,32 @@ from .models import ...
8
10
  router = APIRouter(prefix="/{{ app_name }}", tags=['{{ app_name }}'])
9
11
 
10
12
 
13
+
11
14
  @router.get("/")
12
- def hello_world():
13
- return {
14
- "project": "{{ project_name }}",
15
- "app": "{{ app_name }}",
16
- }
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)
17
41
 
ohmyapi/router.py CHANGED
@@ -1 +1,2 @@
1
1
  from fastapi import APIRouter, Depends, HTTPException
2
+ from http import HTTPStatus
@@ -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
 
@@ -3,14 +3,17 @@ ohmyapi/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
3
3
  ohmyapi/builtin/auth/__init__.py,sha256=vOVCSJX8BALzs8h5ZW9507bjoscP37bncMjdMmBXcMM,42
4
4
  ohmyapi/builtin/auth/models.py,sha256=Fggg3GDVydKoZQOlXXNDsWKxehvsp8BXC1xedv0Qr34,1729
5
5
  ohmyapi/builtin/auth/permissions.py,sha256=mxsnhF_UGesTFle7v1JHORkNODtQ0qanAL3FtOcMCEY,145
6
- ohmyapi/builtin/auth/routes.py,sha256=faVs9FuldubEcA3N335OvNGwiMUBjGxHf4IzIUQhV8o,5744
6
+ ohmyapi/builtin/auth/routes.py,sha256=e6i9AanJqj1RK8VQVXwNV-Pv8R3X8I70SgaDtbCYUIE,6318
7
+ ohmyapi/builtin/demo/__init__.py,sha256=k1rGtOmMPVZJ1fMPELY0v3k70WyzSp18pstJTkCdFr0,42
8
+ ohmyapi/builtin/demo/models.py,sha256=8Id0dHPuow_Dtt_337UV3VTxvtVAllaVeImvstqiqdE,1403
9
+ ohmyapi/builtin/demo/routes.py,sha256=MQaPCz43v4kTv9_oz44up1j77Ra1SqSN9i3vN26xllU,1843
7
10
  ohmyapi/cli.py,sha256=dJVNgpW5S4rCc619AEEKBKuEIAmQs153Ls0ZVaea48w,4173
8
11
  ohmyapi/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
12
  ohmyapi/core/runtime.py,sha256=i4dI7mC59rw0blMOeVGawUm8v5id3ZllLlP8RLo1GT0,9419
10
13
  ohmyapi/core/scaffolding.py,sha256=SA0SYFd7VcqkOn9xuXgj-yOoVqCZMJo68GGFbm34GE4,2663
11
14
  ohmyapi/core/templates/app/__init__.py.j2,sha256=QwVIQVUGZVhdH1d4NrvL7NTsK4-T4cihzYs8UVX2dt4,43
12
15
  ohmyapi/core/templates/app/models.py.j2,sha256=_3w-vFJ5fgsmncsCv34k_wyCMF78jufbSSglns4gbb0,119
13
- ohmyapi/core/templates/app/routes.py.j2,sha256=M2yqfSrhgo_ruGookHbVHoURWKdgqS2H8SgVvHfXmpo,441
16
+ ohmyapi/core/templates/app/routes.py.j2,sha256=dFpmfrfN1pwOsD6MAa_MmI7aP4kKJ2ZiijobWHsfyDs,873
14
17
  ohmyapi/core/templates/project/README.md.j2,sha256=SjR4JIrg-8XRE-UntUDwiw8jDpYitD_UjwoKkYJ7GLw,22
15
18
  ohmyapi/core/templates/project/pyproject.toml.j2,sha256=X0VS6YT9aL3vpHFKPTfLFsdpD8423nY57ySQpSTMxmQ,895
16
19
  ohmyapi/core/templates/project/settings.py.j2,sha256=RBKGB8MZWPM3Bp0a57Y1YrSvSXxh502TUnJqbbu48Ig,138
@@ -18,8 +21,8 @@ ohmyapi/db/__init__.py,sha256=5QKUycxnN83DOUD_Etoee9tEOYjnZ74deqrSOOx_MiQ,204
18
21
  ohmyapi/db/exceptions.py,sha256=vb4IIUoeYAY6sK42zRtjMy-39IFVi_Qb6mWySTY0jYw,34
19
22
  ohmyapi/db/model/__init__.py,sha256=k3StTNuKatpwZo_Z5JBFa-927eJrzibFE8U4SA82asc,32
20
23
  ohmyapi/db/model/model.py,sha256=WTf41ByCtfk9c_O6QCsO9KA0avHL3zGMZ6SEdw5GOuc,2420
21
- ohmyapi/router.py,sha256=VXSnPZnrgOY_h_02vcA4wxBE90bkwiAKgxHgF2MM9cE,54
22
- ohmyapi-0.1.21.dist-info/METADATA,sha256=mvxZbP4doiXu07xbVs2SVqtm3VhowpcnhE2pqG8r-HE,9071
23
- ohmyapi-0.1.21.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
24
- ohmyapi-0.1.21.dist-info/entry_points.txt,sha256=wb3lw8-meAlpiv1mqcQ3m25ukL7djagU_w89GkrC37k,43
25
- ohmyapi-0.1.21.dist-info/RECORD,,
24
+ ohmyapi/router.py,sha256=6Exv6sVPVyiIYxxAQbxQhFRX74MKTUPWXIBwC7UZ-ww,82
25
+ ohmyapi-0.1.23.dist-info/METADATA,sha256=HPmvXFChCOfKmKOn6u6JlcItr_2_UrXk9MoA7aCBl2Y,9727
26
+ ohmyapi-0.1.23.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
27
+ ohmyapi-0.1.23.dist-info/entry_points.txt,sha256=wb3lw8-meAlpiv1mqcQ3m25ukL7djagU_w89GkrC37k,43
28
+ ohmyapi-0.1.23.dist-info/RECORD,,