|
1 | | -from typing import Any, Dict |
| 1 | +from typing import Any, Dict, List |
2 | 2 | from libs.enums import JobStageType, StageStatusType |
3 | 3 | from libs.utils import update_job_status |
4 | 4 | import json |
|
15 | 15 | pg_hook = PostgresHook(postgres_conn_id="postgres_db_conn") |
16 | 16 |
|
17 | 17 |
|
18 | | -def build_rules_json(scan_report_name: str, scan_report_id: int) -> BytesIO: |
19 | | - """ |
20 | | - Builds the rules in JSON format. |
21 | | -
|
22 | | - Args: |
23 | | - - scan_report_name: str, the name of the scan report |
24 | | - - scan_report_id: int, the id of the scan report |
25 | | -
|
26 | | - Returns: |
27 | | - - BytesIO, the rules in JSON format |
28 | | - """ |
29 | | - try: |
30 | | - # Build the metadata for the JSON file |
31 | | - metadata = { |
32 | | - "date_created": datetime.now(timezone.utc).isoformat(), |
33 | | - "dataset": scan_report_name, |
34 | | - } |
35 | | - # Get the all processed rules from the temp table as dataframe in Pandas |
36 | | - processed_rules = pg_hook.get_pandas_df( |
37 | | - "SELECT * FROM temp_rules_export_%(scan_report_id)s_json ORDER BY sr_concept_id;", |
38 | | - parameters={"scan_report_id": scan_report_id}, |
39 | | - ) |
40 | | - |
41 | | - result: Dict[str, Any] = {} |
42 | | - for _, row in processed_rules.iterrows(): |
43 | | - dest_table = row["dest_table"] |
44 | | - concept_key = f"{row['concept_name']} {row['sr_concept_id']}" |
45 | | - dest_field = row["dest_field"] |
46 | | - source_table = row["source_table"].replace("\ufeff", "") |
47 | | - source_field = row["source_field"].replace("\ufeff", "") |
48 | | - |
49 | | - # Prepare the term_mapping value |
50 | | - # will solve #1006 here (fields end with source_concept_id and concept_id will have different values), |
51 | | - # then assign term_mapping step below |
52 | | - if pd.notnull(row["term_mapping_value"]): |
53 | | - term_mapping = {row["term_mapping_value"]: row["concept_id"]} |
54 | | - else: |
55 | | - term_mapping = row["concept_id"] |
56 | | - |
57 | | - # Build the field entry |
58 | | - field_entry = {"source_table": source_table, "source_field": source_field} |
59 | | - # Assign term_mapping |
60 | | - if dest_field.endswith("_concept_id"): |
61 | | - field_entry["term_mapping"] = term_mapping |
62 | | - |
63 | | - result.setdefault(dest_table, {}).setdefault(concept_key, {})[ |
64 | | - dest_field |
65 | | - ] = field_entry |
66 | | - |
67 | | - cdm = { |
68 | | - "metadata": metadata, |
69 | | - "cdm": result, |
70 | | - } |
71 | | - json_data = json.dumps(cdm, indent=6) |
72 | | - json_bytes = BytesIO(json_data.encode("utf-8")) |
73 | | - json_bytes.seek(0) |
74 | | - return json_bytes |
75 | | - except Exception as e: |
76 | | - logging.error(f"Error building rules JSON: {str(e)}") |
77 | | - update_job_status( |
78 | | - scan_report=scan_report_id, |
79 | | - stage=JobStageType.DOWNLOAD_RULES, |
80 | | - status=StageStatusType.FAILED, |
81 | | - details=f"Error building rules JSON for scan report {scan_report_id}: {str(e)}", |
82 | | - ) |
83 | | - raise e |
84 | | - |
85 | | - |
86 | 18 | def build_rules_csv(scan_report_id: int) -> BytesIO: |
87 | 19 | """ |
88 | 20 | Builds the rules in CSV format. |
@@ -210,3 +142,243 @@ def build_rules_csv(scan_report_id: int) -> BytesIO: |
210 | 142 | details=f"Error building rules CSV for scan report {scan_report_id}: {str(e)}", |
211 | 143 | ) |
212 | 144 | raise e |
| 145 | + |
| 146 | + |
| 147 | +def build_rules_json(scan_report_name: str, scan_report_id: int) -> BytesIO: |
| 148 | + """ |
| 149 | + Builds the rules in JSON format. |
| 150 | +
|
| 151 | + Args: |
| 152 | + - scan_report_name: str, the name of the scan report |
| 153 | + - scan_report_id: int, the id of the scan report |
| 154 | +
|
| 155 | + Returns: |
| 156 | + - BytesIO, the rules in JSON format |
| 157 | + """ |
| 158 | + try: |
| 159 | + # Build the metadata for the JSON file |
| 160 | + metadata = { |
| 161 | + "date_created": datetime.now(timezone.utc).isoformat(), |
| 162 | + "dataset": scan_report_name, |
| 163 | + } |
| 164 | + # Get the all processed rules from the temp table as dataframe in Pandas |
| 165 | + processed_rules = pg_hook.get_pandas_df( |
| 166 | + "SELECT * FROM temp_rules_export_%(scan_report_id)s_json ORDER BY sr_concept_id;", |
| 167 | + parameters={"scan_report_id": scan_report_id}, |
| 168 | + ) |
| 169 | + |
| 170 | + result: Dict[str, Any] = {} |
| 171 | + for _, row in processed_rules.iterrows(): |
| 172 | + dest_table = row["dest_table"] |
| 173 | + concept_key = f"{row['concept_name']} {row['sr_concept_id']}" |
| 174 | + dest_field = row["dest_field"] |
| 175 | + source_table = row["source_table"].replace("\ufeff", "") |
| 176 | + source_field = row["source_field"].replace("\ufeff", "") |
| 177 | + |
| 178 | + # Prepare the term_mapping value |
| 179 | + # will solve #1006 here (fields end with source_concept_id and concept_id will have different values), |
| 180 | + # then assign term_mapping step below |
| 181 | + if pd.notnull(row["term_mapping_value"]): |
| 182 | + term_mapping = {row["term_mapping_value"]: row["concept_id"]} |
| 183 | + else: |
| 184 | + term_mapping = row["concept_id"] |
| 185 | + |
| 186 | + # Build the field entry |
| 187 | + field_entry = {"source_table": source_table, "source_field": source_field} |
| 188 | + # Assign term_mapping |
| 189 | + if dest_field.endswith("_concept_id"): |
| 190 | + field_entry["term_mapping"] = term_mapping |
| 191 | + |
| 192 | + result.setdefault(dest_table, {}).setdefault(concept_key, {})[ |
| 193 | + dest_field |
| 194 | + ] = field_entry |
| 195 | + |
| 196 | + cdm = { |
| 197 | + "metadata": metadata, |
| 198 | + "cdm": result, |
| 199 | + } |
| 200 | + json_data = json.dumps(cdm, indent=6) |
| 201 | + json_bytes = BytesIO(json_data.encode("utf-8")) |
| 202 | + json_bytes.seek(0) |
| 203 | + return json_bytes |
| 204 | + except Exception as e: |
| 205 | + logging.error(f"Error building rules JSON: {str(e)}") |
| 206 | + update_job_status( |
| 207 | + scan_report=scan_report_id, |
| 208 | + stage=JobStageType.DOWNLOAD_RULES, |
| 209 | + status=StageStatusType.FAILED, |
| 210 | + details=f"Error building rules JSON for scan report {scan_report_id}: {str(e)}", |
| 211 | + ) |
| 212 | + raise e |
| 213 | + |
| 214 | + |
| 215 | +def build_rules_json_v2(scan_report_name: str, scan_report_id: int) -> BytesIO: |
| 216 | + try: |
| 217 | + metadata = { |
| 218 | + "date_created": datetime.now(timezone.utc).isoformat(), |
| 219 | + "dataset": scan_report_name, |
| 220 | + } |
| 221 | + |
| 222 | + processed_rules = pg_hook.get_pandas_df( |
| 223 | + "SELECT * FROM temp_rules_export_%(scan_report_id)s_json ORDER BY dest_table, source_table, source_field;", |
| 224 | + parameters={"scan_report_id": scan_report_id}, |
| 225 | + ) |
| 226 | + |
| 227 | + result: Dict[str, Any] = {} |
| 228 | + |
| 229 | + for dest_table, dest_table_group in processed_rules.groupby("dest_table"): |
| 230 | + dest_table_str = str(dest_table) |
| 231 | + result[dest_table_str] = {} |
| 232 | + |
| 233 | + for source_table, source_table_group in dest_table_group.groupby( |
| 234 | + "source_table" |
| 235 | + ): |
| 236 | + source_table_clean = str(source_table).replace("\ufeff", "") |
| 237 | + result[dest_table_str][source_table_clean] = {} |
| 238 | + |
| 239 | + person_id_mappings = {} |
| 240 | + date_mappings = {} |
| 241 | + concept_mappings = {} |
| 242 | + |
| 243 | + for source_field, source_field_group in source_table_group.groupby( |
| 244 | + "source_field" |
| 245 | + ): |
| 246 | + source_field_clean = str(source_field).replace("\ufeff", "") |
| 247 | + dest_fields = source_field_group["dest_field"].dropna().unique() |
| 248 | + |
| 249 | + # forming the person_id_mapping |
| 250 | + if "person_id" in [d.lower() for d in dest_fields]: |
| 251 | + person_id_mappings = { |
| 252 | + "source_field": source_field_clean, |
| 253 | + "dest_field": "person_id", |
| 254 | + } |
| 255 | + |
| 256 | + # forming the date_mapping |
| 257 | + date_like_fields = [ |
| 258 | + d |
| 259 | + for d in dest_fields |
| 260 | + if d and d.lower().endswith(("date", "datetime")) |
| 261 | + ] |
| 262 | + if date_like_fields: |
| 263 | + date_mappings = { |
| 264 | + "source_field": source_field_clean, |
| 265 | + "dest_field": date_like_fields, |
| 266 | + } |
| 267 | + |
| 268 | + is_measurement_observation_table = dest_table_str in [ |
| 269 | + "measurement", |
| 270 | + "observation", |
| 271 | + ] |
| 272 | + field_level_mappings: Dict[str, List[int]] = {} |
| 273 | + value_level_mappings: Dict[str, Dict[str, List[int]]] = {} |
| 274 | + |
| 275 | + only_field_mappings = True |
| 276 | + only_value_mappings = True |
| 277 | + |
| 278 | + for _, row in source_field_group.iterrows(): |
| 279 | + dest_field = row["dest_field"] |
| 280 | + concept_id = row["concept_id"] |
| 281 | + term_mapping_value = row.get("term_mapping_value") |
| 282 | + |
| 283 | + # Solved #1006 here |
| 284 | + if pd.notnull(term_mapping_value): |
| 285 | + only_field_mappings = False |
| 286 | + if term_mapping_value not in value_level_mappings: |
| 287 | + value_level_mappings[term_mapping_value] = {} |
| 288 | + if dest_field.endswith("_concept_id"): |
| 289 | + # initialize the dest_field if not exists |
| 290 | + if ( |
| 291 | + dest_field |
| 292 | + not in value_level_mappings[term_mapping_value] |
| 293 | + ): |
| 294 | + value_level_mappings[term_mapping_value][ |
| 295 | + dest_field |
| 296 | + ] = [] |
| 297 | + value_level_mappings[term_mapping_value][ |
| 298 | + dest_field |
| 299 | + ].append(concept_id) |
| 300 | + else: |
| 301 | + only_value_mappings = False |
| 302 | + if dest_field.endswith("_concept_id"): |
| 303 | + # initialize the dest_field if not exists |
| 304 | + if dest_field not in field_level_mappings: |
| 305 | + field_level_mappings[dest_field] = [] |
| 306 | + field_level_mappings[dest_field].append(concept_id) |
| 307 | + |
| 308 | + concept_mapping: Dict[str, Any] = {} |
| 309 | + |
| 310 | + # Forming the concept_mapping based on the mapping types |
| 311 | + if only_field_mappings: |
| 312 | + # Field-only mappings: put everything under "*" key |
| 313 | + concept_mapping["*"] = field_level_mappings |
| 314 | + |
| 315 | + elif only_value_mappings: |
| 316 | + # Value-only mappings: put each value as direct key |
| 317 | + for value, mappings in value_level_mappings.items(): |
| 318 | + if mappings: |
| 319 | + concept_mapping[value] = mappings |
| 320 | + |
| 321 | + else: |
| 322 | + # Mixed mappings: "*" key for field-level + individual value keys |
| 323 | + # NOTE: If there are mappings at the date/pseron field and value_as_concept_id field taken into account, |
| 324 | + # it will be mixed mappings automatically, which will cause issues, so we need to have a way to add this mixed case in one of the above types |
| 325 | + concept_mapping["*"] = field_level_mappings |
| 326 | + |
| 327 | + # For each value, create its own mapping |
| 328 | + for value, value_mappings in value_level_mappings.items(): |
| 329 | + if value_mappings: |
| 330 | + concept_mapping[value] = value_mappings |
| 331 | + |
| 332 | + # Forming the original_value field |
| 333 | + original_values = [ |
| 334 | + f for f in dest_fields if f.endswith("_source_value") |
| 335 | + ] |
| 336 | + # Add value_as_string/number for tables observation and measurement |
| 337 | + if is_measurement_observation_table: |
| 338 | + if "value_as_string" in dest_fields: |
| 339 | + original_values.append("value_as_string") |
| 340 | + if "value_as_number" in dest_fields: |
| 341 | + original_values.append("value_as_number") |
| 342 | + # Add original_value field with unique values |
| 343 | + concept_mapping["original_value"] = list(set(original_values)) |
| 344 | + |
| 345 | + # for a mappings of a source field group to be appear in the result, it must have original_value |
| 346 | + # (which should always have at least one _source_value field) |
| 347 | + if concept_mapping and concept_mapping.get("original_value"): |
| 348 | + concept_mappings[source_field_clean] = concept_mapping |
| 349 | + |
| 350 | + # Adding the person_id_mapping, date_mapping, concept_mapping to the result |
| 351 | + if person_id_mappings: |
| 352 | + result[dest_table_str][source_table_clean][ |
| 353 | + "person_id_mapping" |
| 354 | + ] = person_id_mappings |
| 355 | + |
| 356 | + if date_mappings: |
| 357 | + result[dest_table_str][source_table_clean][ |
| 358 | + "date_mapping" |
| 359 | + ] = date_mappings |
| 360 | + |
| 361 | + if concept_mappings: |
| 362 | + result[dest_table_str][source_table_clean][ |
| 363 | + "concept_mappings" |
| 364 | + ] = concept_mappings |
| 365 | + |
| 366 | + cdm = { |
| 367 | + "metadata": metadata, |
| 368 | + "cdm": result, |
| 369 | + } |
| 370 | + |
| 371 | + json_data = json.dumps(cdm, indent=2) |
| 372 | + json_bytes = BytesIO(json_data.encode("utf-8")) |
| 373 | + json_bytes.seek(0) |
| 374 | + return json_bytes |
| 375 | + |
| 376 | + except Exception as e: |
| 377 | + logging.error(f"Error building rules JSON v2: {str(e)}") |
| 378 | + update_job_status( |
| 379 | + scan_report=scan_report_id, |
| 380 | + stage=JobStageType.DOWNLOAD_RULES, |
| 381 | + status=StageStatusType.FAILED, |
| 382 | + details=f"Error building rules JSON v2 for scan report {scan_report_id}: {str(e)}", |
| 383 | + ) |
| 384 | + raise e |
0 commit comments