prof-rossetti / intro-to-python

An Introduction to Programming in Python
Other
97 stars 244 forks source link

Firebase Notes (NoSQL Database) #82

Open s2t2 opened 3 years ago

s2t2 commented 3 years ago

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

s2t2 commented 2 years ago

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)