This package provides an opinionated SOA-layered fastapi-based web-framework.
Project description
Lilly
Lilly is fast service-oriented and layered Python 3.6+ web framework built on top of FastAPI It is enforces a certain way of creating FastApi applications that is much easier to reason about. Since it is based on FastAPI, it is modern, fast (high performance), and works well with Python type hints.
Purpose
Lilly signifies peaceful beauty. Lilly is thus an opinionated framework that ensures clean beautiful code structure that scales well for large projects and large teams.
- It just adds more opinionated structure to the already beautiful FastAPI.
- It ensures that when someone is building a web application basing on Lilly, they don't need to think about the structure.
- The developer should just know that it is a service-oriented architecture with each service having a layered architecture that ensures layers don't know what the other layer is doing.
Key Features
On top of the key features of FastAPI which include:
- Fast. It is based on FastApi
- Intuitive: Great editor support. Completion everywhere. Less time debugging.
- Easy: Designed to be easy to use and learn. Less time reading docs.
- Short: Minimize code duplication. Multiple features from each parameter declaration. Fewer bugs.
- Robust: Get production-ready code. With automatic interactive documentation.
- Standards-based: Based on (and fully compatible with) the open standards for APIs: OpenAPI (previously known as Swagger) and JSON Schema.
It also:
- Enforces a separation of concerns between service to service
- Enforces a separation of concerns within the service between presentation, business, persistence, and data_source layers
Quick Start
- Ensure you have Python 3.7 or +3.7 installed
- Create a new folder for your application
mkdir lilly_sample && cd lilly_sample
- Create the virtual environment and activate it
python3 -m venv env
source env/bin/activate
- Install lilly
pip install lilly
- Create your first application based off the framework
python -m lilly create-app
This will create the following folder structure with some fully functional sample code
.
├── main.py
├── settings.py
└── services
├── __init__.py
└── hello
├── __init__.py
├── actions.py
├── datasources.py
├── dtos.py
├── repositories.py
└── routes.py
- Install uvicorn and run the app
pip install uvicorn
uvicorn main:app --reload
-
View the OpenAPI docs at http://127.0.0.1:8000/docs
-
For you to add another service in the services folder, run the command:
python -m lilly create-service <service-name>
e.g.
python -m lilly create-service blog
- For more information about the commands, just run the
help
commands
python -m lilly --help
python -m lilly create-app --help
python -m lilly create-service --help
How to Run tests
- Clone the repository
git clone git@github.com:sopherapps/lilly.git
- Create virtual environment for Python 3.7 and above and activate it
python3 -m venv env
source env/bin/activate
- Install requirements
pip install -r requirements.txt
- Run the test command
python -m unittest discover -s test
Design
Requirements
The following features are required.
Configuration
- All services are put in the
services
folder whose import path is passed as a parameter to theLilly
instance during initialization. (Default: folder calledservices
on root of project) - All settings are put as constants in the
settings
python module whose import path is passed toLilly
instance at initialization. (Default:settings.py
on the root of project)
Base Structures
- All services must have the following modules or packages:
routes
(if a package is used, allRouteSet
subclasses must be imported into theroutes.__init__
module)actions
repositories
datasources
dtos
- Just like FastAPI Class-based views (CBV)
routes, Lilly routes (which are technically methods of the Service subclass) should have the
post,get,put,patch...
decorators. The format is exactly as it is in FastAPI. In addition, dependencies can be shared across multiple endpoints of the same service thanks toFastApi CBV
. RouteSet
is the base class of all Routes. It should have the following methods overridden:_do(self, actionCls: Type[Action], *args, **kwargs)
which internally initializes the actionCls and callsrun()
on it
Action
subclasses should have an overriddenrun(self) -> Any
method- The
run(self)
method should be able to access any repositories by directly importing any it needs
- The
Repository
subclasses should have public:get_one(self, record_id: Any, **kwargs) -> Any
method to get one record of idrecord_id
get_many(self, skip: int, limit: int, filters: Dict[Any, Any], **kwargs) -> List[Any]
method to get many records that fulfil thefilters
create_one(self, record: Any, **kwargs) -> Any
method to create one recordcreate_many(self, record: List[Any], **kwargs) -> List[Any]
method to create many recordsupdate_one(self, record_id: Any, new_record: Any, **kwargs) -> Any
method to update one record of idrecord_id
update_many(self, new_record: Any, filters: Dict[Any, Any], **kwargs) -> Any
method to update many records that fulfil thefilters
remove_one(self, record_id: Any, **kwargs) -> Any
method to remove one record of idrecord_id
remove_many(self, filters: Dict[Any, Any], **kwargs) -> Any
method to remove many records that fulfil thefilters
Repository
subclasses should also have the following methods overridden:_get_one(self, datasource_connection: Any, record_id: Any, **kwargs) -> Any
method to get one record of idrecord_id
_get_many(self, datasource_connection: Any, skip: int, limit: int, filters: Dict[Any, Any], **kwargs) -> List[Any]
method to get many records that fulfil thefilters
_create_one(self, datasource_connection: Any, record: Any, **kwargs) -> Any
method to create one record_create_many(self, datasource_connection: Any, record: List[Any], **kwargs) -> List[Any]
method to create many records_update_one(self, datasource_connection: Any, record_id: Any, new_record: Any, **kwargs) -> Any
method to update one record of idrecord_id
_update_many(self, datasource_connection: Any, new_record: Any, filters: Dict[Any, Any], **kwargs) -> Any
method to update many records that fulfil thefilters
_remove_one(self, datasource_connection: Any, record_id: Any, **kwargs) -> Any
method to remove one record of idrecord_id
_remove_many(self, datasource_connection: Any, filters: Dict[Any, Any], **kwargs) -> Any
method to remove many records that fulfil thefilters
_datasource(self) -> DataSource
an @property-decorated method to return the DataSource whoseconnect()
method is to be called in any of the other methods to get its instance._to_output_dto(self, record: Any) -> BaseModel
method which converts any record from the data source raw to DTO for the public methods
DataSource
subclasses should have an overriddenconnect(self)
methoddtos
(Data Transfer Object classes) are subclasses of thepydantic.BaseModel
which are to be used to move data across the layers- Any setting added to the gazetted settings file can be accessed via
lilly.conf.settings.<setting_name>
e.g.lilly.conf.settings.APP_SETTING
Running
- The
Lilly
instance should be run the same way as FastAPI instances are run e.g.
uvicorn main:app # for app defined in the main.py module
Implementation Ideas
- The application is an instance of the
Lilly
class which is a subclass of theFastAPI
class. - To create a
Lilly
instance, we need to pass in the following parameters:- services_path (an import path as string, default is "services")
- settings_path (an import path as string, default is "settings")
- During
Lilly
initialization, all routes are automatically imported usingimportlib.import_module
by concatenating the<services_path>.<service_name>.routes
e.g.services.hello.routes
. - In order to make route definition solely dependent on folder structure, we change
@app.get
decorators to@get
app.get
,app.post
etc. should throwNotImplementedError
errors- The whole app has one instance of the
router: APIRouter
. It is defined in therouting
module. - In that same
routing
module,router.get
,router.post
,router.delete
,router.put
,router.patch
,router.head
,router.options
are all aliased by their post-periodsuffixes
e.g.get
,post
etc. - When initializing in init of Lilly, we fetch the routes in all services then call
self.include_router(router)
. app.mount
should throw anNotImplementedError
error because it complicates the app structure if used to mount other applications, considering the fact that all routes share onerouter
instance.- In order to have a protected method
_do()
to call an action within the routers, we use class-based views from fastapi-utils CBV. - All these class based views will be subclasses of
RouteSet
which has an overridable protected method_do(self, action_cls: Action, *args, **kwargs)
to make a call to any action - All these class based views will have a decorator
@routeset
which is an alias of@cbv(router)
whererouter
is the router common to all routes - All the routes in the app have one router so their endpoints need to be different and explicit since no mounting will be allowed
ToDo
- Set up the abstract methods structure
- Set up the CLI to generate an app
- Set up the CLI to generate a service
- Make repository public
- Package it and publish it
- Add some out-of-the-box base data sources e.g.
- SqlAlchemy
- Redis
- Memcached
- RESTAPI
- GraphQL
- RabbitMQ
- ActiveMQ
- Websockets
- Kafka
- Mongodb
- Couchbase
- DiskCache
- Add some out-of-the-box base repositories e.g.
- SqlAlchemyRepo (RDBM e.g. PostgreSQL, MySQL etc.)
- RedisRepo
- MemcachedRepo
- RESTAPIRepo
- GraphQLRepo
- RabbitMQRepo
- ActiveMQRepo
- WebsocketsRepo
- KafkaRepo
- MongodbRepo
- CouchbaseRepo
- DiskCacheRepo
- Add some out-of-the-box base actions e.g.
- CreateOneAction
- CreateManyAction
- UpdateOneAction
- UpdateManyAction
- ReadOneAction
- ReadManyAction
- DeleteOneAction
- DeleteManyAction
- Add some out-of-the-box base route sets
- CRUDRouteSet
- WebsocketRouteSet
- GraphQLRoute
- Add example code in examples folder
- Todolist (CRUDRouteSet, SqlAlchemyRepo)
- RandomQuotes (WebsocketRouteSet, MongodbRepo) (quotes got from the Bible)
- Clock (WebsocketRouteSet, WebsocketsRepo)
- Set up automatic documentation
- Set up CI via Github actions
- Set up CD via Github actions
- Write about it in hashnode or Medium or both
Inspiration
License
Copyright (c) 2022 Martin Ahindura Licensed under the MIT License
Project details
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.