Quickstart¶
This page is adapted from Quickstart using a server client library released under Creative Commons Attribution 4.0 License. Some code samples are also adapted from the source, which are released under the Apache 2.0 License. This page is part of a repository under MIT License, but does not override some licensing conditions of the Google’s quickstart guide. Please refer to these license for more information.
Retrieve credentials¶
In the GCP Console, go to the Create service account key page.
From the Service account list, select New service account.
In the Service account name field, enter a name.
From the Role list, select Project > Owner.
Note: The Role field authorizes your service account to access resources. You can view and change this field later by using the GCP Console. If you are developing a production app, specify more granular permissions than Project > Owner. For more information, see granting roles to service accounts.
Click Create. A JSON file that contains your key downloads to your computer.
Configure service account¶
Grant roles/cloudfunctions.admin
role if you will use flask-boiler to deploy cloud functions.
(Replace with your own service account information where applicable)
gcloud projects add-iam-policy-binding flask-boiler-testing --member=serviceAccount:firebase-adminsdk-4m0ec@flask-boiler-testing.iam.gserviceaccount.com --role roles/cloudfunctions.admin
Also run, You may need to run
gcloud iam service-accounts add-iam-policy-binding firebase-adminsdk-nztgj@gravitate-backend-testing.iam.gserviceaccount.com --member=$MEMBER --role=roles/iam.serviceAccountUser
(Effect unclear)
Add flask-boiler and the server client library to your app¶
Add the required dependencies and client libraries to your app.
In your project’s requirements.txt,
# Append to requirements, unless repeating existing requirements
google-cloud-firestore
flask-boiler # Not released to pypi yet
Configure virtual environment
pip install virtualenv
virtualenv env
source env/bin/activate
In your project directory,
pip install -r requirements.txt
Create a Document As View¶
In this example, we will build a mediator that forwards domain
models (eg. City/TOK
) to view models (eg. cityView/TOK
).
Both data models are stored in a NoSQL datastore, but only the
view model is intended to be shown to the user. This example
is similar to a stream converter, but you may build something
more advanced by leveraging ViewModel.store to query multiple
domain models across the datastore. The example is located in
examples/city
Configure Project¶
Provide authentication credentials to flask-boiler by moving the json certificate file
to your project directory and specify the path in boiler.yaml
in your current working directory.
app_name: "<Your Firebase App Name>"
debug: True
testing: True
certificate_filename: "<File Name of Certificate JSON>"
In __init__
of your project source root:
from onto.context import Context as CTX
CTX.load()
Declare a Domain Model¶
In models.py
, create a model,
from onto.domain_model import DomainModel
from onto import attrs
class City(DomainModel):
city_name = attrs.bproperty()
country = attrs.bproperty()
capital = attrs.bproperty()
class Meta:
collection_name = "City"
Create Attribute objects for your domain model. These will be converted to a Marshmallow Schema for serialization and deserialization.
class Municipality(City):
pass
class StandardCity(City):
city_state = attrs.bproperty()
regions = attrs.bproperty()
You can create subclasses of City
. By default,
they will be stored in the same collection as City
.
Running a query on City.where
will
query all objects that are of subclass of City
:
City, Municipality, StandardCity
. A query on
Municipality.where
will query all objects of
subclass of Municipality
: Municipality
.
Declare View Model¶
Declare a subclass of Store
first. This object helps you reference
domain models by calling self.store.<domain_model_name>
. In this example,
you should initialize the store with a snapshot you may receive from
the View Mediator.
class CityStore(Store):
city = reference(many=False)
Next, declare a View Model. A View Model has attributes that converts
inner data models to presentable data models for front end. The
doc_ref
attribute chooses where the view model will save to.
class CityView(ViewModel):
name = attrs.bproperty()
country = attrs.bproperty()
@classmethod
def new(cls, snapshot):
store = CityStore()
store.add_snapshot("city", dm_cls=City, snapshot=snapshot)
store.refresh()
return cls(store=store)
@name.getter
def name(self):
return self.store.city.city_name
@country.getter
def country(self):
return self.store.city.country
@property
def doc_ref(self):
return CTX.db.document(f"cityView/{self.store.city.doc_id}")
Declare Mediator Class¶
Protocol.on_create
will be called every time a new document (domain
model) is created in the City/
collection. When you start the server,
on_create
will be invoked once for all existing documents.
class CityViewMediator(ViewMediatorDeltaDAV):
def notify(self, obj):
obj.save()
class Protocol(ProtocolBase):
@staticmethod
def on_create(snapshot: DocumentSnapshot, mediator: ViewMediatorBase):
view = CityView.new(snapshot=snapshot)
mediator.notify(obj=view)
Add Entrypoint¶
In main.py
,
city_view_mediator = CityViewMediator(
query=City.get_query()
)
if __name__ == "__main__":
city_view_mediator.start()
When you create a domain model in City/TOK
,
obj = Municipality.new(
doc_id="TOK", city_name='Tokyo', country='Japan', capital=True)
obj.save()
The framework will generate a view document in cityView/TOK
,
{
'doc_ref': 'cityView/TOK',
'obj_type': 'CityView',
'country': 'Japan',
'name': 'Tokyo'
}
Now, you have the basic app set up.
Create a form service¶
In this example, the user can post a form to
/users/<user_id>/cityForms/<city_id>
and create a new city.
Create Form Class¶
Declare a CityForm
used for user to create a new city.
The function decorated with city.init
will be called to
initialize city
attribute to a blank City
Domain Model
in the default location for property reads: obj._attrs
.
The fields of the blank Domain Model are set through the property
setters. propagate_change
will be called by the mediator
to save the newly created city Domain Model to datastore.
class CityForm(ViewModel):
name = attrs.bproperty()
country = attrs.bproperty()
city_id = attrs.bproperty()
city = attrs.bproperty(initialize=True)
@city.init
def city(self):
self._attrs.city = City.new(doc_id=self.doc_ref.id)
@name.setter
def name(self, val):
self.city.city_name = val
@country.setter
def country(self, val):
self.city.country = val
def propagate_change(self):
self.city.save()
Declare Form Mediator¶
class CityFormMediator(ViewMediatorDeltaDAV):
def notify(self, obj):
obj.propagate_change()
class Protocol(ProtocolBase):
@staticmethod
def on_create(snapshot: DocumentSnapshot, mediator: ViewMediatorBase):
obj = CityForm.new(doc_ref=snapshot.reference)
obj.update_vals(with_raw=snapshot.to_dict())
mediator.notify(obj=obj)
Add Security Rule¶
In your Firestore console, add the following security rule:
match /users/{userId}/{documents=**} {
allow read, write: if request.auth.uid == userId
}
This restricts the ability to post a city to only registered users.
Add Service¶
Now, your main.py
should be,
city_view_mediator = CityViewMediator(
query=City.get_query()
)
city_form_mediator = CityFormMediator(
query=CTX.db.collection_group("cityForms")
)
if __name__ == "__main__":
city_view_mediator.start()
city_form_mediator.start()
When the user creates a document in users/uid1/cityForms/LA
,
{
'country': 'USA',
'name': 'Los Angeles'
}
you should be able to receive the domain model in City/LA
,
{
'cityName': 'Los Angeles',
'country': 'USA',
'doc_id': 'LA',
'doc_ref': 'City/LA',
'obj_type': 'City'
}
and the view model in cityView/LA
,
{
'name': 'Los Angeles',
'country': 'USA',
'doc_ref': 'cityView/LA',
'obj_type': 'CityView',
}
This completes the setup for a simple CQRS read model / form set up for
flask-boiler. The user may create new cities by posting a collection
they own, and view cities by reading cityView
.