"""Neo4j executor class for the Chartmetric Charts tasks.""" import os from neo4j import GraphDatabase from feed_ingestion.util.sentry_util import send_error_or_warning class Neo4jExecutor(): """Helper class to abstract Neo4j operations.""" def __init__(self, feed_name, neo4j_config=None): """Initialize executor. Args: feed_name: the name of the feed e.g. chartmetric_charts neo4j_config: config required for neo4j driver """ self.feed_name = feed_name self.database = None if neo4j_config: self.driver = GraphDatabase.driver( neo4j_config['url'], auth=( neo4j_config['user'], neo4j_config['password']), max_connection_lifetime=3600*15, connection_timeout=3600*2 ) self.database = neo4j_config['database'] def __enter__(self): """Return itself when entering the context.""" return self def __exit__(self, exc_type, exc_value, exc_traceback): """Close the driver when exiting the context.""" if self.driver: self.driver.close() def load_query(self, filename): """Read the Cypher query file and return its content. Args: filename (str): The name of the file to read. Returns: str: The content of the file. """ filepath = os.path.abspath( os.path.join(os.path.dirname(os.path.abspath(__file__)), '../../flows/{}/queries/{}.cypher' .format(self.feed_name, filename))) exists = os.path.exists(filepath) if not exists: raise FileNotFoundError(filepath) with open(filepath, 'r') as query_file: return query_file.read() def parametrize_query(self, template, params): """Add parameters to the query template. Args: template (str): The query template. params (dict): The parameters. Returns: str: The parametrized query. """ query = template for key, value in params.items(): query = query.replace('__{}__'.format(key), value) return query def fetchone(self, query_name, params=None): """Fetch one result. Args: query_name (str): The name of the query. params (dict): Parameters to pass to the query. """ if not params: params = {} template = self.load_query(query_name) query = self.parametrize_query(template, params) with self.driver.session(database=self.database) as session: return session.read_transaction(self.run_query, query)[0] @staticmethod def run_query(tx, query, params=None): """Transaction function to run query.""" params = params or {} result = tx.run(query, **params).single() try: if result.get('errorMessages'): raise Exception('Failed to execute cypher query: {}'.format( result.get('errorMessages'))) return result except AttributeError: return result def execute_query( self, query_name, params={}, neo4j_params=None, retries=3): """Execute a query. Args: query_name (str): The name of the query. params (dict): Parameters for query templating neo4j_params (dict): Parameters to pass to the query. """ template = self.load_query(query_name) query = self.parametrize_query(template, params) with self.driver.session(database=self.database) as session: try: session.write_transaction(self.run_query, query, neo4j_params) except Exception as error: if os.environ.get('SENTRY_DSN'): send_error_or_warning(error) if retries > 0: session.close() return self.execute_query( query_name, params=params, neo4j_params=neo4j_params, retries=retries - 1) raise error def execute_write_query(self, query_name, params=None): """Execute a query. It does not parametrize the query. Instead it sends params as args to neo4j transaction. They can be referenced in cypher with $param_name Args: query_name (str): The name of the query. params (dict): Parameters to pass to the query. """ template = self.load_query(query_name) with self.driver.session(database=self.database) as session: try: session.write_transaction(self.run_query, template, params) except Exception as error: if os.environ.get('SENTRY_DSN'): send_error_or_warning(error) raise error