Package: gena
GENA ·Framework to help to build (web) application faster.gena: generate restful APIs from peewee models (i.e., specification of database table).gena-app: provides basic structure for frontend application (state management, routing).For demo, see thetodolist folder.Getting startedWe are going to build a simple todolist app to demonstrate how to use this framework. See thetodolist folderfor the complete source code.Setup the project:create the python project (server code):poetry new todolist(you needPoetryto run this command)move inside the project folder:cd todolistadd our backend library:poetry add genacreate awwwdirectory containing your frontend code usingcreate-react-app: (yarn create react-app www --template typescript)add our frontend library:cd www; yarn add gena-app; cd ..-- this will modify yourwww/package.jsonfile.modify the build script inwww/package.jsonto tellcreate-react-appto build static files intotodolist/wwwdirectory inside the python package:"build": "BUILD_PATH='../todolist/www' react-scripts build". This allows us to distribute both frontend and backend in a single python package.add"proxy": "http://localhost:5000"to thewww/package.json. This allows us to send the request to the server during development.After this step, you will have the following folder structure:todolist
├── todolist                   # our backend code
│   ├── __init__.py
├── pyproject.toml
├── www                        # our frontend codeBackendDefine database schemaWe usepeewee, an ORM library, to define our database schema.First, let's create a file containing our schema:todolist/models.py. In this simple todo list, we only have one tabletodo_list, which has two fields:checkedandtodo.importosfrompeeweeimportSqliteDatabase,Model,BooleanField,TextFielddb=SqliteDatabase(os.environ["DBFILE"])classTodoList(Model):classMeta:database=dbdb_table="todo_list"checked=BooleanField()todo=TextField()(Optionally) We add the following code to the same file to insert some dummy data to our database for testingifnotos.path.exists(os.environ['DBFILE']):db.create_tables([TodoList],safe=True)TodoList.insert_many([{"checked":False,"todo":"go grocery"},{"checked":False,"todo":"do laundry"},]).execute()Create APIsWith our backend library, creating APIs is as simple as calling a functiongenerate_apiwith the ORM modelTodoList:importosfromgenaimportgenerate_app,generate_apifromtodolist.modelsimportTodoListapp=generate_app(controllers=[generate_api(model)formodelin[TodoList]],pkg_dir=os.path.dirname(__file__))Under the hood,generate_apiuses the specification in the model to automatically generate a blueprint containing the following endpoints (sometimes are called views or controllers):GET /{table_name}: querying records matched a query.GET /{table_name}/<id>: get a record by idHEAD /{table_name}/<id>: whether a record with the given id exists.POST /{table_name}/find_by_ids: querying list of records by their idsPOST /{table_name}: create a recordPUT /{table_name}/<id>: update a recordDELETE /{table_name}/<id>: delete a recordDELETE /{table_name}: truncate the whole table, only available if theenable_truncate_tableparameter is set to beTrue.You can extend the blueprint to add additional endpoints or overwrite some if you needed. Inputs from the users are automatically validate based on the type of the field, or you can provide your own deserializer (whose job is also validate the value) for a particular field via thedeserializerparameter. You can also provide your ownserializerto control how the data should be serialize.The outer functiongenerate_appwill returns the Flask application. It takes three parameterscontrollers: a list ofFlask's blueprintsor a python package, in which the blueprints are discovered automatically. This is useful if you organize your blueprints in separated files. All endpoints defined in the blueprints have url prefix/api(/{table_name}becomes/api/{table_name}).pkg_dir: the path to ourtodolistpackage. Our app will serve the static files from{pkg_dir}/wwwfolder.log_sql_queries: whether to log sql queries when we are in development mode.With the given app, we finally can start our server with:DBFILE=./todolist.db FLASK_APP=todolist/app.py flask run. Note: remember to activate your virtual environment first withpoetry shell.FrontendCreate StoresOur frontend library usesReactand provides state management withMobX) and routing withReactRouterout of the box. In addition, most of the functions are typed so that refactoring and maintaining is easier.To start, we create stores which contains all of the data of our application. A store also contains other functions to create, update, delete, and query records from the server. If you haven't familiar with MobX, please check out theirdocumentation.Let's create a filewww/src/models/TodoListStore.tsand paste the following code:import{SimpleCRUDStore}from"gena-app";import{makeObservable,action}from"mobx";exportinterfaceTodo{id:number;checked:boolean;todo:string;}exportclassTodoListStoreextendsSimpleCRUDStore<number,Todo>{constructor(){super(`/api/todo_list`);makeObservable(this,{toggle:action,});}toggle(item:Todo){item.checked=!item.checked;}}In the above code, we create an interfaceTodomatched with the definition in our database schema. Since we didn't provide custom deserializer, the fields in our frontend is matched exactly with the fields in our database in the backend. Then, we define a storeTodoListStoreextending theSimpleCRUDStorefrom our frontend librarygena-app. This gives us several useful functions out of the box such ascreate,delete,update, andquerythe records from the server. We also define an additional function that will toggle thecheckedvalue of anTodoto show how to extend the store if needed.To use the stores withReact Hooks, we need to following code inwww/src/models/index.tsimportReactfrom"react";import{TodoListStore}from"./TodoListStore";exportconststores={todolistStore:newTodoListStore(),};exporttypeIStore=Readonly<typeofstores>;exportconstStoreContext=React.createContext<IStore>(stores);exportfunctionuseStores():IStore{returnReact.useContext(StoreContext);}export{TodoListStoreasVariableStore};exporttype{Todo}from"./TodoListStore";Routing & ApplicationRouting specifies which component the application should render for a particular URL. This makes it much easier to keep the application logic separated especially when you have lots of pages. Our frontend library enables us to define all the routing in a single place with strong type annotation, so that it's easier to maintain the application's routing and is harder to make mistake.Let's create a filewww/routes.tsxand define our first route:import{NoArgsPathDef}from"gena-app";import{HomePage}from"./pages/HomePage";exportconstroutes={home:newNoArgsPathDef({component:HomePage,pathDef:"/",exact:true,}),};In this route, we say when user open/, we will render a componentHomePage. For now, let's just use a dummyHomePagefor now:exportconstHomePage=()=>{return<p>Helloworld</p>;};With the specified routes and a dummyHomePagecomponent, the final piece is to render and attach our component to the DOM. Inindex.tsx:importReactDOMfrom"react-dom";import{App}from"gena-app";import"./index.css";import{StoreContext,stores}from"./models";importreportWebVitalsfrom"./reportWebVitals";import{routes}from"./routes";ReactDOM.render(<StoreContext.Providervalue={stores}><Approutes={routes}/></StoreContext.Provider>,document.getElementById("root"));StoreContext.Providercomponent enables us to useuseStoreshook we defined earlier in any component, andAppcomponent will render the component that matched with the current URL as specified in our routes.Create TodoList componentIt's now time to complete ourHomePagecomponent to show the TodoList.We first use the stores within the component and wrapped it withobserverso that it will react whenever the state changesimport{observer}from"mobx-react";exportconstHomePage=observer(()=>{const{todolistStore}=useStores();...});We are going to display the todo list as a list. So we useList component.exportconstHomePage=observer(()=>{const{todolistStore}=useStores();useEffect(()=>{todolistStore.fetch({limit:1000,offset:0});},[]);constitems=todolistStore.list.map((item)=>{return(<List.Itemkey={item.id}><Checkboxchecked={item.checked}onChange={(e)=>{item.checked=e.target.checked;todolistStore.update(item);}}><Typography.Paragraphstyle={{marginBottom:0}}editable={{onChange:(text)=>{item.todo=text;todolistStore.update(item);},}}>{item.todo}</Typography.Paragraph></Checkbox><Buttontype="primary"danger={true}onClick={()=>{todolistStore.delete(item.id);}}>Delete</Button></List.Item>);});constaddItem=()=>todolistStore.create({checked:false,todo:""});return(<Spacedirection="vertical"style={{width:"100%"}}><Listbordered={true}>{items}</List><Buttontype="primary"onClick={addItem}>Add</Button></Space>);});Now, we are able to display the list of todo items from the database, and check/uncheck item that is done. We can extend the UI to add other functionalities such as add, delete, or update the content of todo items. You can check out the complete code inhere.