import logging from functools import reduce from itertools import chain from typing import Dict, List, Union from service.tasks.analytics.charts import ( get_chart, get_global_chart_configurations, prep_chart_conf, ) from service.tasks.analytics.services import ( ml_info_tabs, rfm_info_tabs, superfans_info_tabs, ) from service.utils.aws_connectors import pd_read_sql, run_query from service.utils.data_model_utils import get_collection_type, get_related_collections logger = logging.getLogger(__name__) GROUP_CONF: List[Dict[str, Union[str, int]]] = [ { "name": "", "tag": "data_sources", "description": "", }, { "name": "Socio Demographics", "tag": "demographics", "description": "Fan demographics", }, { "name": "Geolocation Intelligence", "tag": "geolocation", "description": "Fan location", }, { "name": "Purchase Intelligence", "tag": "purchase_intelligence", "description": "Analytics about fan purchases", }, { "name": "Superfans", "tag": "superfans", "description": "Analytics about most engaged and active fans", }, { "name": "Valuable Fan Segments by RFM", "tag": "rfm", "description": "Fans recency, frequency & monetary analysis", }, { "name": "Smart Segments from Machine Learning", "tag": "ml", "description": "Actionable fan segments found from machine learning", }, ] for _gid, _conf in enumerate(GROUP_CONF): _conf["id"] = _gid INFO_TABS: Dict[str, Dict[str, Union[str, Dict, List]]] = { "rfm": { "id": "it-rfm", "type": "InfoTabs", "label": "", "options": {}, "data": rfm_info_tabs, }, "ml": { "id": "it-ml", "type": "InfoTabs", "label": "", "options": {}, "data": ml_info_tabs, }, "superfans": { "id": "it-superfans", "type": "InfoTabs", "label": "", "options": {}, "data": superfans_info_tabs, }, } for _, tab_lst in INFO_TABS.items(): for _info_tab_serial, tab in enumerate(tab_lst["data"]): tab["id"] = f"{_info_tab_serial}" _info_tab_serial += 1 for _iid, itm in enumerate(tab["items"]): itm["id"] = f"{_iid}" def get_group_map(group_field: str): group_template = ["id", "name", "description"] groups = {g["tag"]: {key: g[key] for key in group_template} for g in GROUP_CONF} for group in groups.values(): group[group_field] = [] # type: ignore return groups def get_charts(schema: str, collection_id: int = None): """ """ # Fires off the data retrieval in parallel use_cache = True if collection_id is not None: collection_type = get_collection_type(schema, collection_id) if collection_type == "set": use_cache = False return [ get_chart(chart_conf).prepare_data(use_cache=use_cache) for chart_conf in get_available_analytics( schema=schema, collection_id=collection_id ) ] # Waits for all the results and compiles chart objects def get_analytics(schema: str, collection_id: int = None): """Return applicable analytics""" prepared_charts = get_charts(schema, collection_id) # Waits for all the results and compiles chart objects return [chart.json_for_appsync() for chart in prepared_charts] def get_grouped_analytics( schema: str, collection_id: int = None, filter_excessive_charts=() ): """Return applicable analytics in groups filter_excessive_ml=[('group_tag', number_of_elements),..]: filter out groups, that have charts with more than n elements """ prepared_charts = get_charts(schema, collection_id) groups = get_group_map("charts") for chart in prepared_charts: try: chart_data = chart.json_for_appsync() if chart_data["data"]: # leave out empty charts groups[chart.conf["group_tag"]]["charts"].append(chart_data) except Exception as e: logger.exception("CHART FAILED", exc_info=e) result = [] for group_tag, group in groups.items(): group_charts = group["charts"] # make sure, that in group tt every chart has at most limit elements for tt, limit in filter_excessive_charts: if group_tag == tt: for cc in group_charts: if len(cc["data"]) > limit: # reset the charts, don't add group_charts = None break break if group_charts: if group_tag in INFO_TABS: group["charts"] = [INFO_TABS[group_tag]] + group_charts result.append(group) return result def re_pre_calculate_chart_data(schema: str, collection_id: int = None): """Re and/or Pre Calculates query data for affected charts It's assumed, that when collection_id is given, it's a new freshly added/modified collection, so the full data charts need to be recalculated as well :returns True, if all charts were calculated error free, False, if there were some errors. """ recalculating = [ get_chart(chart_conf).recalculate() for chart_conf in get_available_analytics(schema=schema) ] if collection_id is not None: recalculating.extend( get_chart(chart_conf).recalculate() for chart_conf in get_available_analytics( schema=schema, collection_id=collection_id ) ) if recalculating: return reduce( lambda x, y: x and y, [chart.wait_for_done() for chart in recalculating] ) return True def get_available_analytics(schema: str, collection_id: int = None, **kwargs): """Check available fields and return the list of analytics""" # Run analyze on everything we need for analytics :s try: a_tables = [ "attribute", "set_collection", "collection_fan", "fan_attribute", "collection", "collection_attribute", ] sql = f"ANALYZE {','.join([f'{schema}.{table}' for table in a_tables])};" run_query(sql) except Exception as e: logger.exception("ANALYZE FAILED", exc_info=e) """ Get available attributes from the schema""" query = f"SELECT * FROM {schema}.attribute a" attribute_params = {} if collection_id is not None: collection_type = get_collection_type(schema, collection_id) if collection_type in ["source", "enrichment"]: query += ( f" WHERE a.id in (SELECT DISTINCT attribute_id " f"FROM {schema}.collection_attribute ca " f"JOIN {schema}.collection c ON ca.collection_id = c.id " f"WHERE c.id = %(collection_id)s OR c.parent_id = %(collection_id)s)" ) attribute_params["collection_id"] = collection_id kwargs["collection_ids"] = get_related_collections( schema, collection_id, only_types=["source", "enrichment", "algo"] ) elif collection_type in ["set", "segment"]: collection_ids = get_related_collections( schema, collection_id, only_types=["source", "enrichment", "algo"], include_segment_sources=True, ) if collection_ids: coll_ids = ",".join( chain([str(cid) for cid in collection_ids], [str(collection_id)]) ) query += ( f" WHERE a.id in (SELECT DISTINCT attribute_id FROM {schema}.collection_attribute ca " f"JOIN {schema}.collection c ON ca.collection_id = c.id " f"WHERE c.id IN ({coll_ids})" f" OR c.parent_id IN ({coll_ids}) )" ) kwargs["collection_ids"] = collection_ids kwargs["collection_id"] = collection_id fields_df = pd_read_sql(query, params=attribute_params) available_fields_list = { x[0]: x[1] for x in fields_df[["name", "id"]].itertuples(index=False) } for chart in get_global_chart_configurations(): if prep_chart_conf(chart, available_fields_list, schema, kwargs): yield chart