Skip to content

Commit

Permalink
Merge pull request #93 from AfricasVoices/analysis-dashboard-create-s…
Browse files Browse the repository at this point in the history
…napshot

Create analysis snapshot in dashboard
  • Loading branch information
as2388 authored May 30, 2023
2 parents 5305d89 + bb6948d commit 240955e
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 0 deletions.
1 change: 1 addition & 0 deletions analysis_dashboard/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .analysis_dashboard import AnalysisDashboard
97 changes: 97 additions & 0 deletions analysis_dashboard/analysis_dashboard.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
from os.path import getsize

from core_data_modules.logging import Logger
from firebase_admin import firestore, storage

from analysis_dashboard.data_models import AnalysisSnapshot
from util.firebase_utils import initialize_firebase_app

log = Logger(__name__)


class AnalysisDashboard:
def __init__(self, firebase_app):
"""
Client for accessing an Analysis Dashboard Firebase project.
:param firebase_app: Firebase app.
:type firebase_app: firebase_admin.App
"""
self._firebase_app = firebase_app

@classmethod
def init_from_credentials(cls, cert, app_name="AnalysisDashboard"):
"""
Initialises an `AnalysisDashboard` instance from the given Firebase credentials.
The credentials define which Firebase instance to connect to.
:param cert: Firestore service account certificate, as a path to a file or a dictionary.
:type cert: str | dict
:param app_name: Name to give the Firebase app instance used to connect.
:type app_name: str
:return: Initialised AnalysisDashboard instance.
:rtype: AnalysisDashboard
"""
firebase_app = initialize_firebase_app(cert, app_name)
return cls(firebase_app)

def create_snapshot(self, series_id, files, bucket_name):
"""
Creates a new analysis snapshot in Firebase.
:param series_id: Id of the series the snapshot is for.
:type series_id: str
:param files: Files to upload as part of the snapshot, as a dictionary of (local file path) -> (blob name).
:type files: dict of str -> str
:param bucket_name: Name of bucket to upload files to e.g. 'analysis-dashboard.appspot.com'
:type bucket_name: str
"""
snapshot = AnalysisSnapshot(
files=list(files.values())
)

log.info(f"Creating new analysis snapshot with id {snapshot.snapshot_id}...")
for i, (local_file_path, blob_name) in enumerate(files.items()):
log.info(f"Uploading file {i + 1}/{len(files)} to storage")
self.upload_file_to_storage(
file_path=local_file_path,
blob_name=f"series/{series_id}/snapshots/{snapshot.snapshot_id}/files/{blob_name}",
bucket_name=bucket_name
)

log.info(f"Writing analysis snapshot document to Firestore...")
self.create_snapshot_doc_in_firestore(series_id, snapshot)
log.info(f"Created new analysis snapshot with id {snapshot.snapshot_id}")

def create_snapshot_doc_in_firestore(self, series_id, analysis_snapshot):
"""
Writes a snapshot document to the AnalysisDashboard firestore in 'create' mode.
If a snapshot with this snapshot id and series id already exists, this function will fail.
:param series_id: Id of the series this snapshot is for.
:type series_id: str
:param analysis_snapshot: Analysis snapshot document to write.
:type analysis_snapshot: analysis_dashboard.data_models.AnalysisSnapshot
"""
firestore_client = firestore.client(self._firebase_app)
firestore_client \
.document(f"series/{series_id}/snapshots/{analysis_snapshot.snapshot_id}") \
.create(analysis_snapshot.to_dict())

def upload_file_to_storage(self, file_path, blob_name, bucket_name):
"""
Uploads a file from the local disk to an Analysis Dashboard storage bucket.
:param file_path: Path on local disk to the file to upload.
:type file_path: str
:param blob_name: Name to give the blob in storage.
:type blob_name: str
:param bucket_name: Name of the bucket to upload the file to.
:type bucket_name: str
"""
bucket = storage.bucket(bucket_name, app=self._firebase_app)
blob = bucket.blob(blob_name)
log.info(f"Uploading '{file_path}' -> '{blob.public_url}' ({getsize(file_path)} bytes)...")
blob.upload_from_filename(file_path)
1 change: 1 addition & 0 deletions analysis_dashboard/data_models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .analysis_snapshot import AnalysisSnapshot
52 changes: 52 additions & 0 deletions analysis_dashboard/data_models/analysis_snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import uuid


class AnalysisSnapshot:
"""
Represents a single version of a piece of analysis, describing which datasets are available and how those
datasets were generated.
"""

def __init__(self, files, snapshot_id=None):
"""
:param files: List of files available.
:type files: list of str
:param snapshot_id: Id of this analysis snapshot. If None, a message id will automatically be generated in
the constructor.
:type snapshot_id: str | None
TODO: Support tags
"""
if snapshot_id is None:
snapshot_id = str(uuid.uuid4())

self.snapshot_id = snapshot_id
self.files = files

def to_dict(self):
"""
Serializes this snapshot to a dictionary.
:return: Serialized snapshot.
:rtype: dict
"""
return {
"snapshot_id": self.snapshot_id,
"files": self.files,
"tags": [],
"tag_categories": []
}

@classmethod
def from_dict(cls, d):
"""
Initialises an AnalysisSnapshot from a serialized dictionary.
:param d: Dictionary to deserialize.
:type d: dict
:return: Deserialized snapshot.
:rtype: AnalysisSnapshot
"""
return cls(
d["snapshot_id"],
d["files"]
)

0 comments on commit 240955e

Please sign in to comment.