ohmyapi 0.1.20__py3-none-any.whl → 0.1.22__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/__init__.py CHANGED
@@ -1,2 +1 @@
1
1
  from . import db
2
-
@@ -1,4 +1 @@
1
- from . import models
2
- from . import routes
3
- from . import permissions
4
-
1
+ from . import models, permissions, routes
@@ -1,11 +1,12 @@
1
- from ohmyapi.router import HTTPException
2
- from ohmyapi.db import Model, field, pre_save, pre_delete
3
-
4
1
  from functools import wraps
5
- from typing import Optional, List
2
+ from typing import List, Optional
3
+ from uuid import UUID
4
+
6
5
  from passlib.context import CryptContext
7
6
  from tortoise.contrib.pydantic import pydantic_queryset_creator
8
- from uuid import UUID
7
+
8
+ from ohmyapi.db import Model, field, pre_delete, pre_save
9
+ from ohmyapi.router import HTTPException
9
10
 
10
11
  pwd_context = CryptContext(schemes=["argon2"], deprecated="auto")
11
12
 
@@ -22,10 +23,12 @@ class User(Model):
22
23
  password_hash: str = field.CharField(max_length=128)
23
24
  is_admin: bool = field.BooleanField(default=False)
24
25
  is_staff: bool = field.BooleanField(default=False)
25
- groups: field.ManyToManyRelation[Group] = field.ManyToManyField("ohmyapi_auth.Group", related_name="users", through='user_groups')
26
+ groups: field.ManyToManyRelation[Group] = field.ManyToManyField(
27
+ "ohmyapi_auth.Group", related_name="users", through="user_groups"
28
+ )
26
29
 
27
30
  class Schema:
28
- exclude = 'password_hash',
31
+ exclude = ("password_hash",)
29
32
 
30
33
  def set_password(self, raw_password: str) -> None:
31
34
  """Hash and store the password."""
@@ -1,8 +1,8 @@
1
1
  from .routes import (
2
- get_token,
3
2
  get_current_user,
4
- require_authenticated,
3
+ get_token,
5
4
  require_admin,
6
- require_staff,
5
+ require_authenticated,
7
6
  require_group,
7
+ require_staff,
8
8
  )
@@ -3,13 +3,12 @@ from enum import Enum
3
3
  from typing import Any, Dict, List
4
4
 
5
5
  import jwt
6
+ import settings
6
7
  from fastapi import APIRouter, Body, Depends, Header, HTTPException, status
7
8
  from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
8
9
  from pydantic import BaseModel
9
10
 
10
- from ohmyapi.builtin.auth.models import User, Group
11
-
12
- import settings
11
+ from ohmyapi.builtin.auth.models import Group, User
13
12
 
14
13
  # Router
15
14
  router = APIRouter(prefix="/auth", tags=["auth"])
@@ -17,8 +16,12 @@ router = APIRouter(prefix="/auth", tags=["auth"])
17
16
  # Secrets & config (should come from settings/env in real projects)
18
17
  JWT_SECRET = getattr(settings, "JWT_SECRET", "changeme")
19
18
  JWT_ALGORITHM = getattr(settings, "JWT_ALGORITHM", "HS256")
20
- ACCESS_TOKEN_EXPIRE_SECONDS = getattr(settings, "JWT_ACCESS_TOKEN_EXPIRE_SECONDS", 15 * 60)
21
- REFRESH_TOKEN_EXPIRE_SECONDS = getattr(settings, "JWT_REFRESH_TOKEN_EXPIRE_SECONDS", 7 * 24 * 60 * 60)
19
+ ACCESS_TOKEN_EXPIRE_SECONDS = getattr(
20
+ settings, "JWT_ACCESS_TOKEN_EXPIRE_SECONDS", 15 * 60
21
+ )
22
+ REFRESH_TOKEN_EXPIRE_SECONDS = getattr(
23
+ settings, "JWT_REFRESH_TOKEN_EXPIRE_SECONDS", 7 * 24 * 60 * 60
24
+ )
22
25
 
23
26
  oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
24
27
 
@@ -36,30 +39,38 @@ def decode_token(token: str) -> Dict:
36
39
  try:
37
40
  return jwt.decode(token, JWT_SECRET, algorithms=[JWT_ALGORITHM])
38
41
  except jwt.ExpiredSignatureError:
39
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired")
42
+ raise HTTPException(
43
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Token expired"
44
+ )
40
45
  except jwt.InvalidTokenError:
41
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token")
46
+ raise HTTPException(
47
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token"
48
+ )
42
49
 
43
50
 
44
51
  class TokenType(str, Enum):
45
52
  """
46
53
  Helper for indicating the token type when generating claims.
47
54
  """
55
+
48
56
  access = "access"
49
57
  refresh = "refresh"
50
58
 
51
59
 
52
- def claims(token_type: TokenType, user: User, groups: List[Group] = []) -> Dict[str, Any]:
60
+ def claims(
61
+ token_type: TokenType, user: User, groups: List[Group] = []
62
+ ) -> Dict[str, Any]:
53
63
  return {
54
- 'type': token_type,
55
- 'sub': str(user.id),
56
- 'user': {
57
- 'username': user.username,
58
- 'email': user.email,
64
+ "type": token_type,
65
+ "sub": str(user.id),
66
+ "user": {
67
+ "username": user.username,
68
+ "email": user.email,
59
69
  },
60
- 'roles': [g.name for g in groups]
70
+ "roles": [g.name for g in groups],
61
71
  }
62
72
 
73
+
63
74
  async def get_token(token: str = Depends(oauth2_scheme)) -> Dict:
64
75
  """Dependency: token introspection"""
65
76
  payload = decode_token(token)
@@ -71,11 +82,15 @@ async def get_current_user(token: str = Depends(oauth2_scheme)) -> User:
71
82
  payload = decode_token(token)
72
83
  user_id = payload.get("sub")
73
84
  if user_id is None:
74
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload")
85
+ raise HTTPException(
86
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload"
87
+ )
75
88
 
76
89
  user = await User.filter(id=user_id).first()
77
90
  if not user:
78
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
91
+ raise HTTPException(
92
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"
93
+ )
79
94
  return user
80
95
 
81
96
 
@@ -101,15 +116,13 @@ async def require_staff(current_user: User = Depends(get_current_user)) -> User:
101
116
 
102
117
 
103
118
  async def require_group(
104
- group_name: str,
105
- current_user: User = Depends(get_current_user)
119
+ group_name: str, current_user: User = Depends(get_current_user)
106
120
  ) -> User:
107
121
  """Ensure the current user belongs to the given group."""
108
122
  user_groups = await current_user.groups.all()
109
123
  if not any(g.name == group_name for g in user_groups):
110
124
  raise HTTPException(
111
- status_code=403,
112
- detail=f"User must belong to group '{group_name}'"
125
+ status_code=403, detail=f"User must belong to group '{group_name}'"
113
126
  )
114
127
  return current_user
115
128
 
@@ -124,15 +137,21 @@ async def login(form_data: LoginRequest = Body(...)):
124
137
  """Login with username & password, returns access and refresh tokens."""
125
138
  user = await User.authenticate(form_data.username, form_data.password)
126
139
  if not user:
127
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials")
140
+ raise HTTPException(
141
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
142
+ )
128
143
 
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)
144
+ access_token = create_token(
145
+ claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS
146
+ )
147
+ refresh_token = create_token(
148
+ claims(TokenType.refresh, user), REFRESH_TOKEN_EXPIRE_SECONDS
149
+ )
131
150
 
132
151
  return {
133
152
  "access_token": access_token,
134
153
  "refresh_token": refresh_token,
135
- "token_type": "bearer"
154
+ "token_type": "bearer",
136
155
  }
137
156
 
138
157
 
@@ -141,14 +160,20 @@ async def refresh_token(refresh_token: str):
141
160
  """Exchange refresh token for new access token."""
142
161
  payload = decode_token(refresh_token)
143
162
  if payload.get("type") != "refresh":
144
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token")
163
+ raise HTTPException(
164
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid refresh token"
165
+ )
145
166
 
146
167
  user_id = payload.get("sub")
147
168
  user = await User.filter(id=user_id).first()
148
169
  if not user:
149
- raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found")
170
+ raise HTTPException(
171
+ status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found"
172
+ )
150
173
 
151
- new_access = create_token(claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS)
174
+ new_access = create_token(
175
+ claims(TokenType.access, user), ACCESS_TOKEN_EXPIRE_SECONDS
176
+ )
152
177
  return {"access_token": new_access, "token_type": "bearer"}
153
178
 
154
179
 
@@ -161,4 +186,3 @@ async def introspect(token: Dict = Depends(get_token)):
161
186
  async def me(user: User = Depends(get_current_user)):
162
187
  """Return the currently authenticated user."""
163
188
  return User.Schema.one.from_orm(user)
164
-
ohmyapi/cli.py CHANGED
@@ -2,14 +2,17 @@ import asyncio
2
2
  import atexit
3
3
  import importlib
4
4
  import sys
5
+ from getpass import getpass
6
+ from pathlib import Path
7
+
5
8
  import typer
6
9
  import uvicorn
7
10
 
8
- from getpass import getpass
9
- from ohmyapi.core import scaffolding, runtime
10
- from pathlib import Path
11
+ from ohmyapi.core import runtime, scaffolding
11
12
 
12
- app = typer.Typer(help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM.")
13
+ app = typer.Typer(
14
+ help="OhMyAPI — Django-flavored FastAPI scaffolding with tightly integrated TortoiseORM."
15
+ )
13
16
 
14
17
 
15
18
  @app.command()
@@ -78,6 +81,7 @@ def shell(root: str = "."):
78
81
  start_ipython(argv=[], user_ns=shell_vars, config=c)
79
82
  except ImportError:
80
83
  import code
84
+
81
85
  code.interact(local=shell_vars, banner=banner)
82
86
  finally:
83
87
  loop.run_until_complete(cleanup())
@@ -120,11 +124,15 @@ def createsuperuser(root: str = "."):
120
124
  project_path = Path(root).resolve()
121
125
  project = runtime.Project(project_path)
122
126
  if not project.is_app_installed("ohmyapi_auth"):
123
- print("Auth app not installed! Please add 'ohmyapi_auth' to your INSTALLED_APPS.")
127
+ print(
128
+ "Auth app not installed! Please add 'ohmyapi_auth' to your INSTALLED_APPS."
129
+ )
124
130
  return
125
131
 
126
132
  import asyncio
133
+
127
134
  import ohmyapi_auth
135
+
128
136
  email = input("E-Mail: ")
129
137
  username = input("Username: ")
130
138
  password1, password2 = "foo", "bar"
@@ -133,9 +141,10 @@ def createsuperuser(root: str = "."):
133
141
  password2 = getpass("Repeat Password: ")
134
142
  if password1 != password2:
135
143
  print("Passwords didn't match!")
136
- user = ohmyapi_auth.models.User(email=email, username=username, is_staff=True, is_admin=True)
144
+ user = ohmyapi_auth.models.User(
145
+ email=email, username=username, is_staff=True, is_admin=True
146
+ )
137
147
  user.set_password(password1)
138
148
  asyncio.run(project.init_orm())
139
149
  asyncio.run(user.save())
140
150
  asyncio.run(project.close_orm())
141
-
ohmyapi/core/runtime.py CHANGED
@@ -2,16 +2,18 @@
2
2
  import copy
3
3
  import importlib
4
4
  import importlib.util
5
+ import json
5
6
  import pkgutil
6
7
  import sys
7
8
  from pathlib import Path
8
- from typing import Dict, List, Optional
9
+ from typing import Any, Dict, Generator, List, Optional
9
10
 
10
11
  import click
11
12
  from aerich import Command as AerichCommand
12
13
  from aerich.exceptions import NotInitedError
14
+ from fastapi import APIRouter, FastAPI
13
15
  from tortoise import Tortoise
14
- from fastapi import FastAPI, APIRouter
16
+
15
17
  from ohmyapi.db.model import Model
16
18
 
17
19
 
@@ -43,7 +45,9 @@ class Project:
43
45
  orig = importlib.import_module(full)
44
46
  sys.modules[alias] = orig
45
47
  try:
46
- sys.modules[f"{alias}.models"] = importlib.import_module(f"{full}.models")
48
+ sys.modules[f"{alias}.models"] = importlib.import_module(
49
+ f"{full}.models"
50
+ )
47
51
  except ModuleNotFoundError:
48
52
  pass
49
53
 
@@ -51,7 +55,9 @@ class Project:
51
55
  try:
52
56
  self.settings = importlib.import_module("settings")
53
57
  except Exception as e:
54
- raise RuntimeError(f"Failed to import project settings from {self.project_path}") from e
58
+ raise RuntimeError(
59
+ f"Failed to import project settings from {self.project_path}"
60
+ ) from e
55
61
 
56
62
  # Load installed apps
57
63
  for app_name in getattr(self.settings, "INSTALLED_APPS", []):
@@ -103,11 +109,16 @@ class Project:
103
109
  for app_name, app in self._apps.items():
104
110
  modules = list(dict.fromkeys(app.model_modules))
105
111
  if modules:
106
- config["apps"][app_name] = {"models": modules, "default_connection": "default"}
112
+ config["apps"][app_name] = {
113
+ "models": modules,
114
+ "default_connection": "default",
115
+ }
107
116
 
108
117
  return config
109
118
 
110
- def build_aerich_command(self, app_label: str, db_url: Optional[str] = None) -> AerichCommand:
119
+ def build_aerich_command(
120
+ self, app_label: str, db_url: Optional[str] = None
121
+ ) -> AerichCommand:
111
122
  # Resolve label to flat_label
112
123
  if app_label in self._apps:
113
124
  flat_label = app_label
@@ -128,7 +139,7 @@ class Project:
128
139
  return AerichCommand(
129
140
  tortoise_config=tortoise_cfg,
130
141
  app=flat_label,
131
- location=str(self.migrations_dir)
142
+ location=str(self.migrations_dir),
132
143
  )
133
144
 
134
145
  # --- ORM lifecycle ---
@@ -143,7 +154,9 @@ class Project:
143
154
  await Tortoise.close_connections()
144
155
 
145
156
  # --- Migration helpers ---
146
- async def makemigrations(self, app_label: str, name: str = "auto", db_url: Optional[str] = None) -> None:
157
+ async def makemigrations(
158
+ self, app_label: str, name: str = "auto", db_url: Optional[str] = None
159
+ ) -> None:
147
160
  cmd = self.build_aerich_command(app_label, db_url=db_url)
148
161
  async with cmd as c:
149
162
  await c.init()
@@ -157,7 +170,9 @@ class Project:
157
170
  await c.init_db(safe=True)
158
171
  await c.migrate(name=name)
159
172
 
160
- async def migrate(self, app_label: Optional[str] = None, db_url: Optional[str] = None) -> None:
173
+ async def migrate(
174
+ self, app_label: Optional[str] = None, db_url: Optional[str] = None
175
+ ) -> None:
161
176
  labels: List[str]
162
177
  if app_label:
163
178
  if app_label in self._apps:
@@ -218,30 +233,47 @@ class App:
218
233
  pass
219
234
 
220
235
  def __repr__(self):
221
- out = ""
222
- out += f"App: {self.name}\n"
223
- out += f"Models:\n"
224
- for model in self.models:
225
- out += f" - {model.__name__}\n"
226
- out += "Routes:\n"
227
- for route in (self.routes or []):
228
- out += f" - {route}\n"
229
- return out
236
+ return json.dumps(self.dict(), indent=2)
230
237
 
231
238
  def __str__(self):
232
239
  return self.__repr__()
233
240
 
241
+ def _serialize_route(self, route):
242
+ """Convert APIRoute to JSON-serializable dict."""
243
+ return {
244
+ "path": route.path,
245
+ "name": route.name,
246
+ "methods": list(route.methods),
247
+ "endpoint": route.endpoint.__name__, # just the function name
248
+ "response_model": (
249
+ getattr(route, "response_model", None).__name__
250
+ if getattr(route, "response_model", None)
251
+ else None
252
+ ),
253
+ "tags": getattr(route, "tags", None),
254
+ }
255
+
256
+ def _serialize_router(self):
257
+ return [self._serialize_route(route) for route in self.routes]
258
+
259
+ def dict(self) -> Dict[str, Any]:
260
+ return {
261
+ "models": [m.__name__ for m in self.models],
262
+ "routes": self._serialize_router(),
263
+ }
264
+
234
265
  @property
235
- def models(self) -> List[Model]:
236
- models: List[Model] = []
266
+ def models(self) -> Generator[Model, None, None]:
237
267
  for mod in self.model_modules:
238
268
  models_mod = importlib.import_module(mod)
239
269
  for obj in models_mod.__dict__.values():
240
- if isinstance(obj, type) and getattr(obj, "_meta", None) is not None and obj.__name__ != 'Model':
241
- models.append(obj)
242
- return models
270
+ if (
271
+ isinstance(obj, type)
272
+ and getattr(obj, "_meta", None) is not None
273
+ and obj.__name__ != "Model"
274
+ ):
275
+ yield obj
243
276
 
244
277
  @property
245
278
  def routes(self):
246
279
  return self.router.routes
247
-
@@ -1,4 +1,5 @@
1
1
  from pathlib import Path
2
+
2
3
  from jinja2 import Environment, FileSystemLoader
3
4
 
4
5
  # Base templates directory
@@ -8,14 +9,21 @@ env = Environment(loader=FileSystemLoader(str(TEMPLATE_DIR)))
8
9
 
9
10
  def render_template_file(template_path: Path, context: dict, output_path: Path):
10
11
  """Render a single Jinja2 template file to disk."""
11
- template = env.get_template(str(template_path.relative_to(TEMPLATE_DIR)).replace("\\", "/"))
12
+ template = env.get_template(
13
+ str(template_path.relative_to(TEMPLATE_DIR)).replace("\\", "/")
14
+ )
12
15
  content = template.render(**context)
13
16
  output_path.parent.mkdir(exist_ok=True)
14
17
  with open(output_path, "w", encoding="utf-8") as f:
15
18
  f.write(content)
16
19
 
17
20
 
18
- def render_template_dir(template_subdir: str, target_dir: Path, context: dict, subdir_name: str | None = None):
21
+ def render_template_dir(
22
+ template_subdir: str,
23
+ target_dir: Path,
24
+ context: dict,
25
+ subdir_name: str | None = None,
26
+ ):
19
27
  """
20
28
  Recursively render all *.j2 templates from TEMPLATE_DIR/template_subdir into target_dir.
21
29
  If subdir_name is given, files are placed inside target_dir/subdir_name.
@@ -23,14 +31,18 @@ def render_template_dir(template_subdir: str, target_dir: Path, context: dict, s
23
31
  template_dir = TEMPLATE_DIR / template_subdir
24
32
  for root, _, files in template_dir.walk():
25
33
  root_path = Path(root)
26
- rel_root = root_path.relative_to(template_dir) # path relative to template_subdir
34
+ rel_root = root_path.relative_to(
35
+ template_dir
36
+ ) # path relative to template_subdir
27
37
 
28
38
  for f in files:
29
39
  if not f.endswith(".j2"):
30
40
  continue
31
41
 
32
42
  template_rel_path = rel_root / f
33
- output_rel_path = Path(*template_rel_path.parts).with_suffix("") # remove .j2
43
+ output_rel_path = Path(*template_rel_path.parts).with_suffix(
44
+ ""
45
+ ) # remove .j2
34
46
 
35
47
  # optionally wrap in subdir_name
36
48
  if subdir_name:
@@ -54,7 +66,11 @@ def startapp(name: str, project: str):
54
66
  """Create a new app inside a project: templates go into <project_dir>/<name>/"""
55
67
  target_dir = Path(project)
56
68
  target_dir.mkdir(exist_ok=True)
57
- render_template_dir("app", target_dir, {"project_name": target_dir.resolve().name, "app_name": name}, subdir_name=name)
69
+ render_template_dir(
70
+ "app",
71
+ target_dir,
72
+ {"project_name": target_dir.resolve().name, "app_name": name},
73
+ subdir_name=name,
74
+ )
58
75
  print(f"✅ App '{name}' created in project '{target_dir}' successfully.")
59
76
  print(f"🔧 Remember to add '{name}' to your INSTALLED_APPS!")
60
-
@@ -1,17 +1,41 @@
1
- from ohmyapi.router import APIRouter
1
+ from ohmyapi.router import APIRouter, HTTPException, HTTPStatus
2
2
 
3
3
  from . import models
4
4
 
5
+ from typing import List
6
+
5
7
  # Expose your app's routes via `router = fastapi.APIRouter`.
6
8
  # Use prefixes wisely to avoid cross-app namespace-collisions.
7
9
  # Tags improve the UX of the OpenAPI docs at /docs.
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/db/__init__.py CHANGED
@@ -1,10 +1,10 @@
1
- from .model import Model, field
2
1
  from tortoise.manager import Manager
3
2
  from tortoise.queryset import QuerySet
4
3
  from tortoise.signals import (
5
- pre_delete,
6
4
  post_delete,
7
- pre_save,
8
5
  post_save,
6
+ pre_delete,
7
+ pre_save,
9
8
  )
10
9
 
10
+ from .model import Model, field
ohmyapi/db/exceptions.py CHANGED
@@ -1,2 +1 @@
1
1
  from tortoise.exceptions import *
2
-
ohmyapi/db/model/model.py CHANGED
@@ -1,9 +1,10 @@
1
- from pydantic_core import core_schema
1
+ from uuid import UUID
2
+
2
3
  from pydantic import GetCoreSchemaHandler
4
+ from pydantic_core import core_schema
3
5
  from tortoise import fields as field
4
- from tortoise.models import Model as TortoiseModel
5
6
  from tortoise.contrib.pydantic import pydantic_model_creator, pydantic_queryset_creator
6
- from uuid import UUID
7
+ from tortoise.models import Model as TortoiseModel
7
8
 
8
9
 
9
10
  def __uuid_schema_monkey_patch(cls, source_type, handler):
@@ -11,12 +12,16 @@ def __uuid_schema_monkey_patch(cls, source_type, handler):
11
12
  return core_schema.no_info_after_validator_function(
12
13
  # Accept UUID or str, always return UUID internally
13
14
  lambda v: v if isinstance(v, UUID) else UUID(str(v)),
14
- core_schema.union_schema([
15
- core_schema.str_schema(),
16
- core_schema.is_instance_schema(UUID),
17
- ]),
15
+ core_schema.union_schema(
16
+ [
17
+ core_schema.str_schema(),
18
+ core_schema.is_instance_schema(UUID),
19
+ ]
20
+ ),
18
21
  # But when serializing, always str()
19
- serialization=core_schema.plain_serializer_function_ser_schema(str, when_used="always"),
22
+ serialization=core_schema.plain_serializer_function_ser_schema(
23
+ str, when_used="always"
24
+ ),
20
25
  )
21
26
 
22
27
 
@@ -64,4 +69,3 @@ class Model(TortoiseModel, metaclass=ModelMeta):
64
69
  class Schema:
65
70
  include = None
66
71
  exclude = None
67
-
ohmyapi/router.py CHANGED
@@ -1,2 +1,3 @@
1
1
  from fastapi import APIRouter, Depends, HTTPException
2
+ from http import HTTPStatus
2
3
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ohmyapi
3
- Version: 0.1.20
3
+ Version: 0.1.22
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
 
@@ -0,0 +1,25 @@
1
+ ohmyapi/__init__.py,sha256=3y8knoHhOjkUMMi7ADheYxBJywXPvpn5JldxMUeWejw,17
2
+ ohmyapi/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
3
+ ohmyapi/builtin/auth/__init__.py,sha256=vOVCSJX8BALzs8h5ZW9507bjoscP37bncMjdMmBXcMM,42
4
+ ohmyapi/builtin/auth/models.py,sha256=Fggg3GDVydKoZQOlXXNDsWKxehvsp8BXC1xedv0Qr34,1729
5
+ ohmyapi/builtin/auth/permissions.py,sha256=mxsnhF_UGesTFle7v1JHORkNODtQ0qanAL3FtOcMCEY,145
6
+ ohmyapi/builtin/auth/routes.py,sha256=faVs9FuldubEcA3N335OvNGwiMUBjGxHf4IzIUQhV8o,5744
7
+ ohmyapi/cli.py,sha256=dJVNgpW5S4rCc619AEEKBKuEIAmQs153Ls0ZVaea48w,4173
8
+ ohmyapi/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
+ ohmyapi/core/runtime.py,sha256=i4dI7mC59rw0blMOeVGawUm8v5id3ZllLlP8RLo1GT0,9419
10
+ ohmyapi/core/scaffolding.py,sha256=SA0SYFd7VcqkOn9xuXgj-yOoVqCZMJo68GGFbm34GE4,2663
11
+ ohmyapi/core/templates/app/__init__.py.j2,sha256=QwVIQVUGZVhdH1d4NrvL7NTsK4-T4cihzYs8UVX2dt4,43
12
+ ohmyapi/core/templates/app/models.py.j2,sha256=_3w-vFJ5fgsmncsCv34k_wyCMF78jufbSSglns4gbb0,119
13
+ ohmyapi/core/templates/app/routes.py.j2,sha256=dFpmfrfN1pwOsD6MAa_MmI7aP4kKJ2ZiijobWHsfyDs,873
14
+ ohmyapi/core/templates/project/README.md.j2,sha256=SjR4JIrg-8XRE-UntUDwiw8jDpYitD_UjwoKkYJ7GLw,22
15
+ ohmyapi/core/templates/project/pyproject.toml.j2,sha256=X0VS6YT9aL3vpHFKPTfLFsdpD8423nY57ySQpSTMxmQ,895
16
+ ohmyapi/core/templates/project/settings.py.j2,sha256=RBKGB8MZWPM3Bp0a57Y1YrSvSXxh502TUnJqbbu48Ig,138
17
+ ohmyapi/db/__init__.py,sha256=5QKUycxnN83DOUD_Etoee9tEOYjnZ74deqrSOOx_MiQ,204
18
+ ohmyapi/db/exceptions.py,sha256=vb4IIUoeYAY6sK42zRtjMy-39IFVi_Qb6mWySTY0jYw,34
19
+ ohmyapi/db/model/__init__.py,sha256=k3StTNuKatpwZo_Z5JBFa-927eJrzibFE8U4SA82asc,32
20
+ ohmyapi/db/model/model.py,sha256=WTf41ByCtfk9c_O6QCsO9KA0avHL3zGMZ6SEdw5GOuc,2420
21
+ ohmyapi/router.py,sha256=LDxOyiqSb5zVU9zCxi014Ad4DKPZn6V8o9u0WfpmgdE,83
22
+ ohmyapi-0.1.22.dist-info/METADATA,sha256=NrxMn9DH5ydLr6Aui5yN4svdRP9UiuKT5GqzJ8XMuaU,9727
23
+ ohmyapi-0.1.22.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
24
+ ohmyapi-0.1.22.dist-info/entry_points.txt,sha256=wb3lw8-meAlpiv1mqcQ3m25ukL7djagU_w89GkrC37k,43
25
+ ohmyapi-0.1.22.dist-info/RECORD,,
@@ -1,25 +0,0 @@
1
- ohmyapi/__init__.py,sha256=UmLNQImTbKvHEgwQB2Wsyl6fq88X92imL9QZYJpQX4I,18
2
- ohmyapi/__main__.py,sha256=wcCrL4PjG51r5wVKqJhcoJPTLfHW0wNbD31DrUN0MWI,28
3
- ohmyapi/builtin/auth/__init__.py,sha256=TY1RKgwWmJ6FKz_v4J3m0Ang69qSmtVDLe4rqjLk4-E,69
4
- ohmyapi/builtin/auth/models.py,sha256=gkbTRHUg1WgVP_5baDE5NGs8C2ZW1xV68gENAdXA0DA,1712
5
- ohmyapi/builtin/auth/permissions.py,sha256=jf-I2b9rIOw2EF4Kga-_Bz1ZPPHU0vHNaXGrDQSwhSI,145
6
- ohmyapi/builtin/auth/routes.py,sha256=re3w7fZ9q9tUXDsQtkFcKizuEmtjxnebVA2lIO7afkM,5545
7
- ohmyapi/cli.py,sha256=hFhsXArco7R6plYyZ2cdwpRghG2bDiKqIjqs0z1ugv0,4128
8
- ohmyapi/core/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
- ohmyapi/core/runtime.py,sha256=l5zffc4VTwQOo7G9mfiYHsPGapMONRX_jtO_z9QaKHU,8577
10
- ohmyapi/core/scaffolding.py,sha256=Wf-cTC6l2iZETrIbLTT2l6lrNqgI67BSAO4V1nWQPhA,2539
11
- ohmyapi/core/templates/app/__init__.py.j2,sha256=QwVIQVUGZVhdH1d4NrvL7NTsK4-T4cihzYs8UVX2dt4,43
12
- ohmyapi/core/templates/app/models.py.j2,sha256=_3w-vFJ5fgsmncsCv34k_wyCMF78jufbSSglns4gbb0,119
13
- ohmyapi/core/templates/app/routes.py.j2,sha256=MoaAgzTkbr0GbuEic_fKn0PkCLERK8bB-mDlssOxYzE,438
14
- ohmyapi/core/templates/project/README.md.j2,sha256=SjR4JIrg-8XRE-UntUDwiw8jDpYitD_UjwoKkYJ7GLw,22
15
- ohmyapi/core/templates/project/pyproject.toml.j2,sha256=X0VS6YT9aL3vpHFKPTfLFsdpD8423nY57ySQpSTMxmQ,895
16
- ohmyapi/core/templates/project/settings.py.j2,sha256=RBKGB8MZWPM3Bp0a57Y1YrSvSXxh502TUnJqbbu48Ig,138
17
- ohmyapi/db/__init__.py,sha256=T7AUWlEXD--WOn01mKPIpkxPVwmgYHZQ4vQo3ejOkgE,204
18
- ohmyapi/db/exceptions.py,sha256=I7AubrdqQF_UvAvzKqz2ve08-BkXHzEWXnwG300StHE,35
19
- ohmyapi/db/model/__init__.py,sha256=k3StTNuKatpwZo_Z5JBFa-927eJrzibFE8U4SA82asc,32
20
- ohmyapi/db/model/model.py,sha256=ozmrn36swG5Q6SKuS45a46uNoFVJGarNdQRKgOL-7Ao,2364
21
- ohmyapi/router.py,sha256=hutccsrP9RT8W5O6uBDhOJehwqrkRoPzaUI5zoHPh9A,55
22
- ohmyapi-0.1.20.dist-info/METADATA,sha256=JucHuyMQKNNZbnCaorE1ja8kDZHfdl1sjhSIx7a5hsI,9071
23
- ohmyapi-0.1.20.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
24
- ohmyapi-0.1.20.dist-info/entry_points.txt,sha256=wb3lw8-meAlpiv1mqcQ3m25ukL7djagU_w89GkrC37k,43
25
- ohmyapi-0.1.20.dist-info/RECORD,,