"""Library for our CDN Manifests. Reads from a CDN manifest and finds assets based on the application name, the filename. Manifests can be localized, so if the user language is provided, it will do its best to find the corresponding matches. Usage: manifest = Manifest(application_name, environment) url = manifest.get_url('filename') url = manifest.get_url('filename', localize='fr-FR') """ import json import time import boto3 ENVIRONMENT_PROD = 'prod' ENVIRONMENT_QA = 'qa' DEFAULT_LOCALIZATION = 'en' MANIFEST_PATH = 'manifest.json' VERSIONED_PATH = '{timestamp}/{path}' CDN_URLS = { ENVIRONMENT_QA: 'https://qa-cdn.theorchard.io/{app}/{path}', ENVIRONMENT_PROD: 'https://cdn.theorchard.io/{app}/{path}', } BUCKETS = { ENVIRONMENT_QA: 'qa-orcd-cdn', ENVIRONMENT_PROD: 'prod-orcd-cdn' } s3 = boto3.resource('s3') class Manifest: """CDN Manifest. Properties: application_name (str): the name of the application. environment (str): the name of the environment. content (dict): content of the manifest. timestamp (int): the timestamp. """ def __init__(self, application_name, environment, expires_in=None): """Initialize a manifest. Args: application_name (str): the name of the application. It should match the name of the folder in our cdn. environment (str): environment. expires_in (int): time to live in seconds. This makes the manifest live in the memory of the application. If no expires_in is provided, the manifest is fetched once when the application is launched. """ self.application_name = application_name self.environment = validate_environment(environment) self.expires_in = expires_in or 0 self.last_sync = 0 self.timestamp = None self.content = {} def synchronize(self): """Synchronize the manifest. The synchronization process can be done once for all (under the condition that no expires_in is provided), or it could be refreshed at specific intervals. This allows updates deployed to the CDN to be immediatelly captured without having to restart the application. """ if self.timestamp and not self.expires_in: return if self.last_sync + self.expires_in > time.time(): return self.timestamp, self.content = get_manifest( self.application_name, self.environment) self.last_sync = time.time() def get_url(self, filename, localization=None): """Get a url for a file in the manifest. Please note: if the filename is not found, we will simply use the filename and use it as the path. Args: filename (str): the name of the file. localization (str): optional localization field. Returns: str: the url of the asset. """ self.synchronize() path = self.content.get(filename) if isinstance(path, dict): path = path.get(localization) or path.get(DEFAULT_LOCALIZATION) path = path or filename return create_url( self.application_name, self.environment, self.timestamp, path) def create_url(application_name, environment, timestamp, path): """Create a full url. This url is generated for a specific environment, application name, timestamp, and path. Args: application_name (str): the application's name. environment (str): the environment. timestamp (int): the manifest timestamp. path (str): the path fo the file. Returns: str: the full url. """ path = VERSIONED_PATH.format(timestamp=timestamp, path=path) return CDN_URLS.get(environment).format( app=application_name, path=path) def get_manifest(application_name, environment): """Get the manifest from the environment. Args: application_name (str): the name of the application. environment (str): the application's environment. Raises: Exception: if CDN is not reachable, we throw an exception - as we have no way to display those assets. Returns: dict: the content of the manifest. """ path = '/'.join([application_name, MANIFEST_PATH]) bucket = BUCKETS.get(environment) manifest = s3.Object(bucket, path).get()['Body'].read() content = json.loads(manifest.decode('utf8')) return content.pop('timestamp'), content def validate_environment(environment): """Validate an environment. Args: environment (str): the name of the environment. Returns: str: the environment. """ if environment not in [ENVIRONMENT_PROD, ENVIRONMENT_QA]: return ENVIRONMENT_QA return environment