|
| 1 | +#!/usr/bin/env python |
| 2 | +""" |
| 3 | +Copy traffic signal preventative maintenance (PM) records for the primary signal to secondary signal(s) |
| 4 | +""" |
| 5 | +import argparse |
| 6 | +from datetime import date |
| 7 | +import os |
| 8 | + |
| 9 | +import knackpy |
| 10 | + |
| 11 | +from config.knack import CONFIG |
| 12 | +import utils |
| 13 | + |
| 14 | +APP_ID = os.getenv("KNACK_APP_ID") |
| 15 | +API_KEY = os.getenv("KNACK_API_KEY") |
| 16 | + |
| 17 | + |
| 18 | +def generate_field_name_lookup(record): |
| 19 | + lookup = {} |
| 20 | + for field in record.field_defs: |
| 21 | + lookup[field.key] = field.name |
| 22 | + return lookup |
| 23 | + |
| 24 | + |
| 25 | +def find_knack_field_name(field_name, field_map): |
| 26 | + """ |
| 27 | + Returns the knack field name for a given column name |
| 28 | + """ |
| 29 | + for field in field_map: |
| 30 | + if field_map[field] == field_name: |
| 31 | + return field |
| 32 | + raise Exception("Field name not found in field map.") |
| 33 | + |
| 34 | + |
| 35 | +def record_to_knack(record, object_id, method): |
| 36 | + if method == "update": |
| 37 | + assert "id" in record |
| 38 | + res = knackpy.api.record( |
| 39 | + app_id=APP_ID, |
| 40 | + api_key=API_KEY, |
| 41 | + obj=object_id, |
| 42 | + method=method, |
| 43 | + data=record, |
| 44 | + ) |
| 45 | + return res |
| 46 | + |
| 47 | + |
| 48 | +def main(args): |
| 49 | + # Parse Arguments |
| 50 | + app_name = args.app_name |
| 51 | + container = args.container |
| 52 | + logger.info(args) |
| 53 | + |
| 54 | + # Selecting correct config for the view |
| 55 | + config = CONFIG[app_name][container] |
| 56 | + app = knackpy.App(app_id=APP_ID, api_key=API_KEY) |
| 57 | + |
| 58 | + # 1. Check for work orders to be copied. |
| 59 | + # Note that this view is already pre-filtered in Knack to PM records that need to be copied. |
| 60 | + pm_records = app.get(container) |
| 61 | + if not pm_records: |
| 62 | + logger.info("No PM records need to be copied, did nothing.") |
| 63 | + return 0 |
| 64 | + pm_field_names = generate_field_name_lookup(pm_records[0]) |
| 65 | + logger.info(f"{len(pm_records)} PM records to be copied.") |
| 66 | + |
| 67 | + # 2. Download primary signal -> secondary signal relationships |
| 68 | + secondary_key = config["secondary_signals_field"] |
| 69 | + |
| 70 | + # Filtering out traffic signals without secondary signals |
| 71 | + filters = { |
| 72 | + "match": "and", |
| 73 | + "rules": [ |
| 74 | + { |
| 75 | + "field": secondary_key, |
| 76 | + "operator": "is not blank", |
| 77 | + "field_name": "SECONDARY_SIGNALS", |
| 78 | + } |
| 79 | + ], |
| 80 | + } |
| 81 | + records = app.get(config["signals_container"], filters=filters) |
| 82 | + signal_field_names = generate_field_name_lookup(records[0]) |
| 83 | + |
| 84 | + # Creating a lookup dict of primary -> secondary signals |
| 85 | + signal_lookup = {} |
| 86 | + for signal_rec in records: |
| 87 | + if signal_rec.data[secondary_key]: |
| 88 | + signal_lookup[signal_rec.data["id"]] = signal_rec.data[ |
| 89 | + f"{secondary_key}_raw" |
| 90 | + ] |
| 91 | + |
| 92 | + # 3. Do for each PM record: |
| 93 | + # - Set the PM record's COPIED_TO_SECONDARY to True |
| 94 | + # - For each secondary signal, copy the PM record and attach the appropriate signal |
| 95 | + # - Update the modified date of the secondary traffic signal, so the data is refreshed on the ODP. |
| 96 | + current_date = date.today().strftime("%Y-%m-%d") |
| 97 | + |
| 98 | + # Grabbing some useful knack field names |
| 99 | + copied_field_name = find_knack_field_name("COPIED_TO_SECONDARY", pm_field_names) |
| 100 | + signal_field_name = find_knack_field_name("signal", pm_field_names) |
| 101 | + modified_date_field_name = find_knack_field_name( |
| 102 | + "MODIFIED_DATE", signal_field_names |
| 103 | + ) |
| 104 | + |
| 105 | + knack_todos = [] |
| 106 | + for pm in pm_records: |
| 107 | + # Set the PM record's COPIED_TO_SECONDARY to True |
| 108 | + data = {"id": pm.data["id"], copied_field_name: True} |
| 109 | + knack_todos.append({"method": "update", "obj": config["object"], "data": data}) |
| 110 | + pm.data[copied_field_name] = True |
| 111 | + |
| 112 | + # For each secondary signal, copy the PM record and attach the appropriate signal |
| 113 | + primary_signal_id = pm.data[f"{signal_field_name}_raw"][0]["id"] |
| 114 | + secondary_signals = signal_lookup[primary_signal_id] |
| 115 | + for secondary in secondary_signals: |
| 116 | + new_rec = {} |
| 117 | + keys = pm.keys() |
| 118 | + keys.remove("id") |
| 119 | + # Copying PM record, we copy the existing value based on the field type. |
| 120 | + for key in keys: |
| 121 | + if pm.fields[key].field_def.type in ["multiple_choice"]: |
| 122 | + new_rec[key] = pm.data[f"{key}_raw"] |
| 123 | + elif pm.fields[key].field_def.type in ["connection"]: |
| 124 | + new_rec[key] = [entry["id"] for entry in pm.data[f"{key}_raw"]] |
| 125 | + elif pm.fields[key].field_def.type in [ |
| 126 | + "auto_increment", |
| 127 | + "concatenation", |
| 128 | + ]: |
| 129 | + continue |
| 130 | + else: |
| 131 | + new_rec[key] = pm.data[key] |
| 132 | + # Tagging the signal with the secondary signal's knack record ID |
| 133 | + new_rec[signal_field_name] = secondary["id"] |
| 134 | + knack_todos.append( |
| 135 | + {"method": "create", "obj": config["object"], "data": new_rec} |
| 136 | + ) |
| 137 | + |
| 138 | + # Update the modified date of the secondary traffic signal, so the data is refreshed on the ODP. |
| 139 | + data = {"id": secondary["id"], modified_date_field_name: current_date} |
| 140 | + knack_todos.append( |
| 141 | + {"method": "update", "obj": config["signal_object_id"], "data": data} |
| 142 | + ) |
| 143 | + |
| 144 | + # Sending updates/creates to knack |
| 145 | + logger.info(f"Updating/creating {len(knack_todos)} knack records") |
| 146 | + count = 0 |
| 147 | + for knack_job in knack_todos: |
| 148 | + if count % 10 == 0: |
| 149 | + logger.info(f"Uploading record {count} of {len(knack_todos)}") |
| 150 | + res = record_to_knack( |
| 151 | + record=knack_job["data"], |
| 152 | + object_id=knack_job["obj"], |
| 153 | + method=knack_job["method"], |
| 154 | + ) |
| 155 | + count += 1 |
| 156 | + |
| 157 | + |
| 158 | +if __name__ == "__main__": |
| 159 | + # CLI arguments definition |
| 160 | + parser = argparse.ArgumentParser() |
| 161 | + |
| 162 | + parser.add_argument( |
| 163 | + "-a", |
| 164 | + "--app-name", |
| 165 | + type=str, |
| 166 | + help="str: Name of the Knack App in knack.py config file", |
| 167 | + ) |
| 168 | + |
| 169 | + parser.add_argument( |
| 170 | + "-c", |
| 171 | + "--container", |
| 172 | + type=str, |
| 173 | + help="str: AKA API view that was created for downloading the location data", |
| 174 | + ) |
| 175 | + |
| 176 | + args = parser.parse_args() |
| 177 | + |
| 178 | + logger = utils.logging.getLogger(__file__) |
| 179 | + |
| 180 | + main(args) |
0 commit comments