11import logging
2+ import humps # noqa
3+ import uuid
24
5+ from collections .abc import AsyncGenerator
36from datetime import date , datetime # Entity field type
47
5- import humps # noqa
6- from fastapi import FastAPI
7- from fastapi_pagination import add_pagination
8+ from importlib .metadata import version as _version , PackageNotFoundError
9+ from fastapi import FastAPI , Depends , Response , status
10+ from fastapi_pagination import add_pagination , Params
11+ from fastapi_pagination .ext .sqlalchemy import paginate as sa_paginate
12+ from fastapi_users import FastAPIUsers
13+ from fastapi_users .db import SQLAlchemyBaseUserTableUUID , SQLAlchemyUserDatabase
14+ from pydantic_settings import BaseSettings , SettingsConfigDict
15+ from sqlalchemy .orm import DeclarativeBase
16+ from sqlalchemy .ext .asyncio import AsyncSession , async_sessionmaker , create_async_engine
17+ from sqlmodel import Field , SQLModel , Session , create_engine , select
818
19+ from .decorators import action
920from .resource import RouterGenerator
10- from .utils import pluralize
21+ from .utils import pluralize , make_optional_model
1122
12- from .decorators import action
1323
14- from sqlmodel import Field , SQLModel , Session , create_engine , select
24+ try :
25+ __version__ = _version ("balify" )
26+ except PackageNotFoundError :
27+ # fallback for local editable installs or when package metadata not available
28+ __version__ = "0.0.0"
29+
1530
31+ # Constats flags
32+ auth = "auth"
1633
17- sqlite_file_name = "database.db"
18- sqlite_url = f"sqlite:///{ sqlite_file_name } "
1934
35+ # Read database config from `.env` or envirenment
36+ class Settings (BaseSettings ): # type: ignore
37+ model_config = SettingsConfigDict (env_file = ".env" , env_file_encoding = "utf-8" )
2038
21- engine = create_engine (sqlite_url , echo = True )
39+ database_url : str = "sqlite:///database.db"
40+
41+
42+ settings = Settings ()
43+
44+ print ("--> Read database url: %s" % settings .database_url )
45+ engine = create_engine (settings .database_url , echo = True )
2246
2347
2448def create_db_and_tables ():
@@ -33,14 +57,23 @@ class _OMeta(type):
3357 TODO: Compare `Metaclass` and `__init_subclass__`, then choose one in for `_OMeta`
3458 """
3559
60+ # TODO: FastAPI-Users liftspan in `auth.py`, just like `add_pagination`
3661 _app = FastAPI ()
3762
3863 def __new__ (cls , * args , ** kwargs ):
3964 meta = super ().__new__ (cls , * args , ** kwargs )
4065
41- meta ._app = FastAPI ()
66+ print ("--> _OMeta _app: %s" % id (cls ._app ))
67+
4268 meta ._app = add_pagination (cls ._app )
4369
70+ print ("--> add_pagination meta._app: %s" % id (meta ._app ))
71+
72+ # Register FastAPI-Users routers
73+ from .auth import add_users
74+
75+ meta ._app = add_users (meta ._app )
76+
4477 return meta
4578
4679 @property
@@ -75,27 +108,31 @@ class O(metaclass=_OMeta):
75108 """
76109
77110 schema = None # the schema is SQLModel instance
111+ dependencies = [] # router depends
78112
79113 @classmethod
80114 def serve (cls , * entities ) -> None :
81-
82- from fastapi import APIRouter
83-
84- router = APIRouter ()
85-
86- @router .get ("/" )
87- def hello ():
88- return {"Hello" : "World" , "Powered by" : "balify router" }
89-
90- cls ._app .include_router (router , prefix = "/router1" )
91-
115+ print ("--> serve App(%s)" % id (cls ._app ))
92116 for entity in entities :
93117 print ("--> Serve entity `%s` in App(%s)" % (str (entity ), id (cls ._app )))
94- cls ._app .include_router (entity .as_router (), prefix = "/users" )
118+ cls ._app .include_router (
119+ entity .as_router (), prefix = f"/{ pluralize (entity .__name__ .lower ())} "
120+ )
95121
96122 # Generate all SQLModel schemas to database
97123 create_db_and_tables ()
98124
125+ @classmethod
126+ def depends (cls , * args , ** kwargs ):
127+ """Depends build-in depends"""
128+
129+ from .auth import current_active_user
130+
131+ if auth in args :
132+ cls .dependencies = [Depends (current_active_user )]
133+
134+ return cls
135+
99136 @action ()
100137 def list (self ):
101138 """Generic `list` method
@@ -107,9 +144,8 @@ def list(self):
107144 """
108145 with Session (engine ) as session :
109146 statement = select (self .schema )
110- targets = session .exec (statement ).all ()
111- print ("--> Generic list method get targets: %s" % targets )
112- return targets
147+ # targets = session.exec(statement).all()
148+ return sa_paginate (session , statement , Params (page = 1 , size = 10 ))
113149
114150 @action ()
115151 def get (self , pk = None ):
@@ -137,7 +173,10 @@ def create(self, schema_in):
137173 # session.add(schema_in)
138174
139175 # Option 2: Create New schema instance
140- target = self .schema (** schema_in .model_dump ()) # type: ignore
176+ # Why use model_validate?
177+ # Because SQLModel not validate data when `table=True`
178+ # ref: https://github.com/fastapi/sqlmodel/issues/453
179+ target = self .schema .model_validate (schema_in .model_dump ()) # type: ignore
141180 session .add (target )
142181 session .commit ()
143182 session .refresh (target )
@@ -154,7 +193,9 @@ def update(self, schema_in=None, pk=None):
154193 statement = select (self .schema ).where (self .schema .id == pk ) # type: ignore
155194 target = session .exec (statement ).first ()
156195
157- for k , v in schema_in .model_dump ().items (): # type: ignore
196+ optional_model = make_optional_model (self .schema ) # type: ignore
197+ optional_schema = optional_model .model_validate (schema_in .model_dump ()) # type: ignore
198+ for k , v in optional_schema .model_dump ().items (): # type: ignore
158199 if v is not None :
159200 setattr (target , k , v )
160201
@@ -169,10 +210,14 @@ def delete(self, pk=None):
169210 with Session (engine ) as session :
170211 statement = select (self .schema ).where (self .schema .id == pk ) # type: ignore
171212 target = session .exec (statement ).first ()
172- session .delete (target )
173- session .commit ()
174-
175- return {"result" : True }
213+ if target :
214+ session .delete (target )
215+ session .commit ()
216+
217+ # In previou `bali-core`, it return {"result": True} to compatible with gRPC
218+ # It can be simpler with 204 status response
219+ # return {"result": True}
220+ return Response (status_code = status .HTTP_204_NO_CONTENT )
176221
177222
178223# I found that `O, o` in `from balify import O, o` look like an cute emontion.
0 commit comments