Package: cst-micro-chassis
Cst-Micro-ChassisCst-Micro-Chassis is achassis frameworkfor creating REST-ful APIs based on Flask and Flask-Restful.The main topics covered are:A uniform API response formatAPI paginationLoggingHealth checkInstallationpipinstallcst-micro-chassisUsageImport and initialize with a flask app (as you would usually do with any other flask extension)fromflaskimportFlaskfromcst_micro_chassisimportCstMicroChassisapp=Flask('my-first-cst-app')CstMicroChassis.init_app(app)doing this will initialize the CstMicroChassis core class, setting up the default logging format across your app together with a ready-to-use health check endpoint.The default url route for the health check endpoint is/status, so doing acurl localhost/statusshould return{"name":"My-First-Cst-App","version":"N/A"}withContent-Type: application/jsonSetting a custom name and version for the application can be done by setting theCST_PROJECT_NAMEandCST_PROJECT_VERSION, either as environment variables or flask config variables.You may also want to set a custom url route instead of the standard/status; this can be done settingCST_HEALTH_CHECK_ENDPOINT=/my-custom-statusenvironment or flask variable.If the flask application also uses Flask-SQLAlchemy + migrate and alembic, you can also provide the Flask-SQLAlchemy instance as an optional argument to the chassis init_app method like this# import Flask, SQLAlchemy, Migrate, CstMicroChassisapp=Flask('my-first-cst-app-with-db')db=SQLAlchemy()migrate=Migrate()db.init_app()migrate.init_app(app,db)CstMicroChassis.init_app(app,db)doing now acurl localhost/statusshould return also the latest migration from thealembic_versiondatabase table:{"name":"My-First-Cst-App-With-DB","version":"N/A","last_migration":"ef123ae24f13"}LOGGINGAs mentioned above, a custom logging configuration was set for your flask application, so when using the logging module:importlogginglogger=logging.getLogger(__name__)logger.warning('Hello CST')the stdout output will be:2021-02-22 16:42:54,923 [WARNING][No operation_id] myapp: Hello CSTmatching the format<datetime> [<log_level>][<operation_id>] <module_name>: <message>By default, all messages with a severity ofINFOor above (WARNING,ERRORorCRITICAL) will reach stdout. In order to reduce the verbosity of the application, one can setCST_STREAM_LOG_LEVELenvironment variable to a higher severity level i.e. settingCST_STREAM_LOG_LEVEL=ERRORwill only displayERRORandCRITICALlog records.Probably the less obvious section in the logger format above is theoperation_idpart. This is a custom logging attribute bound to the flask application context, more details about it in the RESOURCES section below.RESOURCESApiResource is an enhanced version ofFlask-RESTful's resourcebuilding block.
One would use it in the same manner as a Flask-RESTful resource:importloggingfromflaskimportFlaskfromflask_restfulimportApifromcst_micro_chassisimportCstMicroChassisfromcst_micro_chassis.resourcesimportApiResourceapp=Flask('new-app')CstMicroChassis().init_app(app)logger=logging.getLogger(__name__)api=Api(app)classMySimpleResource(ApiResource):defget(self,simple_id):logger.info(f'I received a GET request for{simple_id}')returnf'It works for{simple_id}'api.add_resource(MySimpleResource,'/simple/<string:simple_id>')if__name__=='__main__':app.run(debug=True)making a get request to this"/simple"endpoint, will return :curllocalhost:5000/simple/123{"meta":{"operation_id":"79bfcecd-55ba-4f97-b175-b104962cafa2"},"data":"It works for 123"}The most obvious difference (compared to a standard response expected from a flask-RESTful resource get method) is the response body, which has two main sections:"meta"and"data".The"data"section contains the actual result of the get operation, which in our naive example, is adding the "It works for " prefix to the simple_id argument which has a value of 123.The"meta"section contains by default anoperation_id, which is a unique uuid4 identifier assigned to each http request. Also looking at flask servers logs, one can notice that between the log level[INFO]and the application name "new-app", also the logging entry's operation_id section was populated with the same value of79bfcecd-55ba-4f97-b175-b104962cafa22021-01-0111:35:01,419[INFO][79bfcecd-55ba-4f97-b175-b104962cafa2]new-app:IreceivedaGETrequestfor123This unique identifier eases debugging by allowing log grouping, aggregation and overall a better tracking on each operation made via the API endpoints.More metadata can be added to thismetasection, like: filtering, sorting or pagination params - as it will be detailed in the following sectionPAGINATIONAssuming the application uses SQLAlchemy ORM, one can use the cst-micro-chassis pagination mixin like this:#assuming there is a model Product, which has at least a few tens of rows saved in dbfromflask_sqlalchemyimportSQLAlchemydb=SQLAlchemy()classProduct(db.Model):id=db.Column(db.String,unique=True,primary_key=True,default=lambdax:str(uuid4()))name=db.Column(db.String)created_at=db.Column(db.DateTime)creating a paginated endpoint to expose all these products, its as easy as:fromcst_micro_chassis.resourcesimportApiResource,ApiPaginatedMixinclassProductsResource(ApiPaginatedMixin,ApiResource):defget(self):products=self.get_paginated_query_result(Product.query.order_by(Product.created_at.asc()))serialized=[{'name':p.name,'created':p.created_at.isoformat()}forpinproducts]returnserialized,200api=Api(app)api.add_resource(ProductsResource,'/products')Behind the scene, this will setup a basic pagination with a default of 25 items per page, which will automatically slice the results ofProduct.query.order_by(Product.created_at.asc())query , and populate the meta section in the response accordingly with anextandprevlink:curl localhost/products/will return{"meta":{"operation_id":"32013480-c882-46dd-a9d6-a931a8535cbe","pagination":{"next":"http://localhost:5000/products/?offset=25&limit=25","prev":null}},"data":[...]}The default number of items per page, which defaults to 25 , can be changed by setting theCST_API_DEFAULT_PAGE_SIZEenvironment variable.Full code example for pagination (manual installation of flask_sqlalchemy is needed)#app.pyimportdatetimefromflaskimportFlaskfromflask_restfulimportApifromflask_sqlalchemyimportSQLAlchemyfromuuidimportuuid4fromcst_micro_chassisimportCstMicroChassisfromcst_micro_chassis.resourcesimportApiResource,ApiPaginatedMixinapp=Flask('new-app')app.config['SQLALCHEMY_DATABASE_URI']='sqlite:///new-app.db'app.config['SQLALCHEMY_TRACK_MODIFICATIONS']=Falsedb=SQLAlchemy()chassis=CstMicroChassis()db.init_app(app)chassis.init_app(app)api=Api(app)classProduct(db.Model):id=db.Column(db.String,unique=True,primary_key=True,default=lambdax:str(uuid4()))name=db.Column(db.String)# ..created_at=db.Column(db.DateTime)classProductsResource(ApiPaginatedMixin,ApiResource):defget(self):products=self.get_paginated_query_result(Product.query.order_by(Product.created_at.asc()))serialized=[{'name':p.name,'created':p.created_at.isoformat()}forpinproducts]returnserialized,200api.add_resource(ProductsResource,'/products')if__name__=='__main__':withapp.app_context():db.create_all()ifProduct.query.count()==0:foriinrange(107):db.session.add(Product(name=f'product No.{i}',created_at=datetime.datetime.utcnow()))db.session.commit()app.run(debug=True)Contributingrun the tests:maketestthis will set up a virtual environment withtoxand run the unittest on Python3.9If one wants to run the test using a different interpreter version, one can use the pyversion argument for themakecommand like this:make pyversion=py38 test(clearly, that interpreter version must be installed on the host otherwise tox will raise anERROR: InterpreterNotFound: python..)