intro-to-python
intro-to-python copied to clipboard
Firebase Notes (NoSQL Database)
We already have some mongodb notes for basic NoSQL, but let's also add notes about firebase.
Setup
https://firebase.google.com/docs/firestore/quickstart
Usage
Something like this:
import os
from firebase_admin import credentials, initialize_app, firestore #, auth
CREDENTIALS_FILEPATH = os.path.join(os.path.dirname(__file__), "..", "google-credentials.json")
creds = credentials.Certificate(CREDENTIALS_FILEPATH)
app = initialize_app(creds) # or set FIREBASE_CONFIG variable and initialize without creds
db = firestore.client()
cities_ref = db.collection('cities')
#ADD
data = {...}
cities_ref.add(data) # auto id?
# DOC REF
cities_ref.document() # auto id
city_ref = cities_ref.document('LA') # specify id
# 3) SET / MERGE DATA
# city_ref.set({...})
# city_ref.set({...}, merge=True)
# 4) UPDATE DATA (JUST SOME FIELDS)
city_ref.update({"toplevel": 4, "favorites.color": "blue"})
# 6) ARRAY APPEND / REMOVE
city_ref.update({'regions': firestore.ArrayUnion(['greater_virginia'])})
city_ref.update({'regions': firestore.ArrayRemove(['east_coast'])})
See: Managing Data - Firebase Python Docs
Also, for higher-level data modeling decisions (whether to store data in a nested document or in a separate collection), see videos like Data Modeling in Firebase.
https://firebase.google.com/docs/firestore/manage-data/structure-data
This is an implementation example, with collections called "calendars" and "events" :
import os
from dateutil.parser import parse as to_dt
from dateutil.tz import gettz as get_tz
from firebase_admin import credentials, initialize_app, firestore #, auth
from dotenv import load_dotenv
load_dotenv()
DEFAULT_FILEPATH = os.path.join(os.path.dirname(__file__), "..", "google-credentials.json")
CREDENTIALS_FILEPATH = os.getenv("GOOGLE_APPLICATION_CREDENTIALS", default=DEFAULT_FILEPATH)
class FirebaseService:
def __init__(self):
self.creds = credentials.Certificate(CREDENTIALS_FILEPATH)
self.app = initialize_app(self.creds) # or set FIREBASE_CONFIG variable and initialize without creds
self.db = firestore.client()
def fetch_authorized_calendars(self, email_address):
# see: https://firebase.google.com/docs/firestore/query-data/queries#query_operators
# ... https://firebase.google.com/docs/firestore/query-data/queries#query_limitations
# there is no logical OR query in firestore, so we have to fetch both and de-dup...
authorizedQuery = self.calendars_ref.where("authorizedUsers", "array_contains", email_address)
#adminQuery = self.calendars_ref.where("adminUsers", "array_contains", email_address)
#calendar_docs = list(authorizedQuery.stream()) + list(adminQuery.stream())
# but actually just add the admin into the authorized list for now
calendar_docs = list(authorizedQuery.stream())
# let's return the dictionaries, so these are serializable (and can be stored in the session)
calendars = []
for calendar_doc in calendar_docs:
calendar = calendar_doc.to_dict()
calendar["id"] = calendar_doc.id
calendars.append(calendar)
return calendars
@property
def calendars_ref(self):
"""Returns a (google.cloud.firestore_v1.collection.CollectionReference) """
return self.db.collection("calendars")
#def add_nested_events(self, events, calendar_id=None, calendar_ref=None):
# """
# Add event records to the given calendar document.
#
# Pass a calendar ref if possible, otherwise pass a calendar id and a new ref will be created.
#
# Params:
# calendar_ref (google.cloud.firestore_v1.document.DocumentReference) : the document reference object
#
# calendar_id (str) : the document id
#
# events (list of dict) : list of events to add, like [{
# "event_start": datetime.datetime object,
# "event_end": datetime.datetime object,
# "reservation": None
# }]
#
# Note: passing datetime objects gives us a timestamp object in firebase!
#
# Returns : (google.cloud.firestore_v1.types.write.WriteResult)
# """
# calendar_ref = calendar_ref or self.calendars_ref.document(calendar_id)
# return calendar_ref.update({"events": firestore.ArrayUnion(events)})
@property
def events_ref(self):
"""Returns a (google.cloud.firestore_v1.collection.CollectionReference) """
return self.db.collection("events")
def create_events(self, events):
"""
Creates new event documents from event data.
Params:
events (list of dict) : list of events to add, like [{
"calendar_id": (str), # referencing the calendar document id
"event_start": datetime.datetime object,
"event_end": datetime.datetime object,
"reservation": None
}]
Note: passing datetime objects gives us a timestamp object in firebase!
See: https://firebase.google.com/docs/firestore/manage-data/transactions#batched-writes
"""
batch = self.db.batch()
for event_data in events:
event_ref = self.events_ref.document() # give each event its own unique id (so we can more easily look it up later!!)
batch.set(event_ref, event_data)
return batch.commit()
def fetch_daily_events(self, calendar_id, date, timezone="US/Eastern"):
"""Params: date (str) like '2021-01-01'"""
tz = get_tz(timezone)
day_start = to_dt(f"{date} 00:00").astimezone(tz)
day_end = to_dt(f"{date} 11:59").astimezone(tz)
query_ref = self.events_ref
query_ref = query_ref.where("calendar_id", "==", calendar_id)
query_ref = query_ref.where("event_start", ">=", day_start)
query_ref = query_ref.where("event_start", "<=", day_end)
event_docs = list(query_ref.stream()) #> list of DocSnapshot
events = []
for event_doc in event_docs:
event = event_doc.to_dict()
event["id"] = event_doc.id
event["event_start"] = event["event_start"].astimezone(tz) # otherwise this will show up as UTC in the browser.
event["event_end"] = event["event_end"].astimezone(tz) # otherwise this will show up as UTC in the browser
events.append(event)
return events
if __name__ == "__main__":
from pprint import pprint
service = FirebaseService()
print("CALENDARS...")
calendars_ref = service.db.collection("calendars")
calendars = [doc.to_dict() for doc in calendars_ref.stream()]
pprint(calendars)