Skip to content

Commit 995185d

Browse files
authored
Merge pull request #5 from fedspendingtransparency/mod/dev-14868-fix-defc-models
[DEV-14868] DEFCGold -> CSVModel Fix
2 parents 0cd434a + 5a9bab6 commit 995185d

8 files changed

Lines changed: 145 additions & 134 deletions

File tree

brus_backend_common/models/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from brus_backend_common.models.lakehouse_model import LakeHouseCurrentMigration, ExternalDataLoadDate
22

3-
from brus_backend_common.models.reference import DEFCSilver, DEFCGroup, DEFCBronze
3+
from brus_backend_common.models.reference import DEFCBronze, DEFCGold, DEFCGroup
44
from brus_backend_common.config import CONFIG
55

66
LAKEHOUSE_BUCKETS = {
@@ -13,7 +13,7 @@
1313
LAKEHOUSE_MODEL_CLASSES = [
1414
DEFCBronze,
1515
DEFCGroup,
16-
DEFCSilver,
16+
DEFCGold,
1717
LakeHouseCurrentMigration,
1818
ExternalDataLoadDate,
1919
]

brus_backend_common/models/lakehouse_model.py

Lines changed: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import ast
12
import importlib.util
23
import io
34
import logging
@@ -11,6 +12,7 @@
1112
from datetime import datetime
1213

1314
import deltalake
15+
import numpy as np
1416
import pyarrow as pa
1517
import pandas as pd
1618
import polars as pl
@@ -21,6 +23,7 @@
2123
from pyspark.sql.functions import monotonically_increasing_id
2224
from pyspark.sql.utils import AnalysisException
2325
from pyspark.sql.types import (
26+
ArrayType,
2427
IntegerType,
2528
StructField,
2629
StringType,
@@ -371,8 +374,22 @@ def _recreate_blank_file(self):
371374
self._s3_client.upload_file(blank_csv, self.BUCKET_NAME, self.RELATIVE_CSV_PATH)
372375

373376
def to_pandas_df(self, **kwargs: Any) -> pd.DataFrame | None:
377+
converters = {}
378+
379+
# Convert arrays from csv format to lists
380+
for col in self.STRUCTURE:
381+
if isinstance(col.dataType, ArrayType):
382+
converters[col.name] = ast.literal_eval
383+
374384
# Type Checker struggles with BytesIO and S3 Objects
375-
return pd.read_csv(io.BytesIO(self._s3_object), **kwargs) if self.exists() else None # type: ignore
385+
df = pd.read_csv(io.BytesIO(self._s3_object), converters=converters, **kwargs) if self.exists() else None # type: ignore
386+
387+
# Convert lists in df to np.arrays
388+
for col in self.STRUCTURE:
389+
if isinstance(col.dataType, ArrayType):
390+
df[col.name] = df[col.name].apply(np.array)
391+
392+
return df
376393

377394
def to_polars_df(self, **kwargs: Any) -> pl.DataFrame | pl.Series | None:
378395
return pl.read_csv(self.CSV_PATH, **kwargs) if self.exists() else None
@@ -394,7 +411,7 @@ def save(self, df: pd.DataFrame | pl.DataFrame) -> None:
394411

395412
class LakeHouseCurrentMigration(CSVModel):
396413
BUCKET_NAME = CONFIG.LAKEHOUSE_REFERENCE_BUCKET
397-
DATABASE_NAME = LakeHouseDatabase.BRONZE
414+
DATABASE_NAME = LakeHouseDatabase.GOLD
398415
TABLE_NAME = "migrations"
399416
DESCRIPTION = "Keeps track of migrations for all Lakehouse Models"
400417
CSV_NAME = "current_migrations.csv"
@@ -415,7 +432,7 @@ class LakeHouseCurrentMigration(CSVModel):
415432

416433
class ExternalDataLoadDate(CSVModel):
417434
BUCKET_NAME = CONFIG.LAKEHOUSE_REFERENCE_BUCKET
418-
DATABASE_NAME = LakeHouseDatabase.BRONZE
435+
DATABASE_NAME = LakeHouseDatabase.GOLD
419436
TABLE_NAME = "external_data_load_date"
420437
DESCRIPTION = "Keeps track of load dates of certain external data Lakehouse models"
421438
CSV_NAME = "external_load_date.csv"

brus_backend_common/models/reference.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
TimestampType,
99
)
1010
from brus_backend_common.config import CONFIG
11-
from brus_backend_common.models.lakehouse_model import DeltaModel, CSVModel, LakeHouseDatabase
11+
from brus_backend_common.models.lakehouse_model import CSVModel, LakeHouseDatabase
1212

1313

1414
class DEFCBronze(CSVModel):
@@ -31,7 +31,7 @@ class DEFCBronze(CSVModel):
3131

3232
class DEFCGroup(CSVModel):
3333
BUCKET_NAME = CONFIG.LAKEHOUSE_REFERENCE_BUCKET
34-
DATABASE_NAME = LakeHouseDatabase.SILVER
34+
DATABASE_NAME = LakeHouseDatabase.GOLD
3535
TABLE_NAME = "defc_mapping"
3636
DESCRIPTION = "Internal CSV to dynamically group DEFCs together"
3737
CSV_NAME = "DEFC_MAPPING.csv"
@@ -47,15 +47,15 @@ class DEFCGroup(CSVModel):
4747
)
4848

4949

50-
class DEFCSilver(DeltaModel):
50+
class DEFCGold(CSVModel):
5151
BUCKET_NAME = CONFIG.LAKEHOUSE_REFERENCE_BUCKET
52-
DATABASE_NAME = LakeHouseDatabase.SILVER
52+
DATABASE_NAME = LakeHouseDatabase.GOLD
5353
TABLE_NAME = "defc"
5454
DESCRIPTION = "DEFC data after initial processing"
55+
CSV_NAME = "def_codes.csv"
5556
PK = "defc_id"
5657
UNIQUE_CONSTRAINTS = ["code"]
5758
MIGRATION_HISTORY = []
58-
5959
STRUCTURE = StructType(
6060
[
6161
StructField("created_at", TimestampType(), True),

brus_backend_common/scripts/loaders/load_defc.py renamed to brus_backend_common/scripts/loaders/defc_gold.py

Lines changed: 100 additions & 102 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
88
import re
99
from datetime import datetime
1010

11-
from brus_backend_common.models import DEFCBronze, DEFCSilver, DEFCGroup
11+
from brus_backend_common.models import DEFCBronze, DEFCGroup, DEFCGold
1212
from brus_backend_common.models.lakehouse_model import update_external_data_load_date
1313
from brus_backend_common.helpers.aws import _get_boto3
14-
from brus_backend_common.helpers.spark import SparkScriptSession
1514
from brus_backend_common.helpers.pandas import check_dataframe_diff
1615
from brus_backend_common.helpers.scripts import (
1716
clean_data,
@@ -203,107 +202,106 @@ def main(local_file: str | None = None, force_reload: bool = False, metrics_json
203202

204203
s3 = _get_boto3("client", "s3")
205204

206-
with SparkScriptSession() as spark:
207-
raw_model = DEFCBronze()
208-
if not raw_model.exists():
209-
raise ValueError(f"{raw_model.TABLE_REF} doesn't exist. Use create_migrate_delta_table beforehand.")
210-
211-
group_model = DEFCGroup()
212-
if not group_model.exists():
213-
raise ValueError(f"{group_model.TABLE_REF} doesn't exist. Use create_migrate_delta_table beforehand.")
214-
215-
int_model = DEFCSilver(spark=spark)
216-
if not int_model.exists():
217-
raise ValueError(f"{int_model.TABLE_REF} doesn't exist. Use create_migrate_delta_table beforehand.")
218-
219-
start_time = datetime.now()
220-
metrics_json["start_time"] = str(start_time)
221-
222-
logger.info("Parsing DEFC data")
223-
try:
224-
if not local_file:
225-
raw_data = raw_model.to_pandas_df(dtype=str, na_filter=False)
226-
else:
227-
raw_data = pd.read_csv(local_file, dtype=str, na_filter=False)
228-
except pd.errors.EmptyDataError:
229-
metrics_json["blank_file"] = True
230-
metrics_json["exit_code"] = 4 # exit code chosen arbitrarily, to indicate distinct failure states
231-
return metrics_json
232-
headers = set([header.upper() for header in list(raw_data)])
233-
234-
if not VALID_HEADERS.issubset(headers):
235-
logger.error("Missing required headers. Required headers include: %s" % str(VALID_HEADERS))
236-
metrics_json["exit_code"] = 4
237-
return metrics_json
238-
metrics_json["records_received"] = len(raw_data)
239-
# Creating a dataframe of the export csv first and then copying columns to match the database
240-
raw_data = raw_data.rename(columns={"DEFC_CODE": "DEFC", "DEFC_TITLE": "Public Law"})
241-
242-
group_model_df = group_model.to_pandas_df()
243-
group_mapping = group_model_df.groupby("group")["code"].agg(list).to_dict()
244-
245-
raw_data = apply_defc_derivations(raw_data, group_mapping)
246-
247-
raw_data = add_defc_outliers(raw_data, group_mapping)
248-
249-
# Clear any lingering np.nan's
250-
raw_data = raw_data.replace({np.nan: None})
251-
252-
logger.info("Checking for differences in DEFC data")
253-
defc_mapping = {
254-
"defc": "code",
255-
"public_law": "public_laws",
256-
"public_law_short_title": "public_law_short_titles",
257-
"group_name": "group",
258-
"urls": "urls",
259-
"is_valid": "is_valid",
260-
"earliest_public_law_enactment_date": "earliest_pl_action_date",
261-
}
262-
data = clean_data(raw_data, defc_mapping, {})
263-
diff_found = check_dataframe_diff(data, int_model.to_pandas_df(), ["defc_id"], ["code"], date_format="%Y-%m-%d")
264-
if force_reload or diff_found:
265-
266-
# The only diff should be whenever a new code is added. Noting it here
267-
if diff_found:
268-
incoming_defcs = list(data["code"])
269-
curr_defcs = list(int_model.to_pandas_df()["code"])
270-
diff_defcs = list(set(incoming_defcs) - set(curr_defcs))
271-
metrics_json["new_defc"] = diff_defcs
272-
logger.info(f"Difference found: {diff_defcs}")
273-
274-
logger.info("Overwriting new DEFC data to Broker")
275-
int_model.save(data)
276-
277-
update_external_data_load_date(int_model, start_time, datetime.now())
278-
logger.info("{} records inserted to DEFC".format(len(data)))
279-
280-
# convert the arrays to pipe-delimited strings
281-
defc_delim = "|"
282-
array_cols = ["Public Law", "Public Law Short Title", "URLs"]
283-
for array_col in array_cols:
284-
raw_data[array_col] = raw_data[array_col].apply(lambda value: defc_delim.join(value))
285-
286-
header_order = [
287-
"DEFC",
288-
"Public Law",
289-
"Public Law Short Title",
290-
"Group Name",
291-
"URLs",
292-
"Is Valid",
293-
"Earliest Public Law Enactment Date",
294-
]
295-
raw_data = raw_data[header_order]
296-
export_name = "def_codes.csv"
297-
logger.info("Exporting loaded DEFC file to {}".format(export_name))
298-
raw_data.to_csv(export_name, index=0)
299-
300-
s3.upload_file(export_name, CONFIG.PUBLIC_FILES_BUCKET, export_name)
301-
302-
os.remove(export_name)
205+
raw_model = DEFCBronze()
206+
if not raw_model.exists():
207+
raise ValueError(f"{raw_model.TABLE_REF} doesn't exist. Use create_migrate_delta_table beforehand.")
208+
209+
group_model = DEFCGroup()
210+
if not group_model.exists():
211+
raise ValueError(f"{group_model.TABLE_REF} doesn't exist. Use create_migrate_delta_table beforehand.")
212+
213+
gold_model = DEFCGold()
214+
if not gold_model.exists():
215+
raise ValueError(f"{gold_model.TABLE_REF} doesn't exist. Use create_migrate_delta_table beforehand.")
216+
217+
start_time = datetime.now()
218+
metrics_json["start_time"] = str(start_time)
219+
220+
logger.info("Parsing DEFC data")
221+
try:
222+
if not local_file:
223+
raw_data = raw_model.to_pandas_df(dtype=str, na_filter=False)
303224
else:
304-
logger.info("No differences found, skipping defc table reload.")
225+
raw_data = pd.read_csv(local_file, dtype=str, na_filter=False)
226+
except pd.errors.EmptyDataError:
227+
metrics_json["blank_file"] = True
228+
metrics_json["exit_code"] = 4 # exit code chosen arbitrarily, to indicate distinct failure states
229+
return metrics_json
230+
headers = set([header.upper() for header in list(raw_data)])
231+
232+
if not VALID_HEADERS.issubset(headers):
233+
logger.error("Missing required headers. Required headers include: %s" % str(VALID_HEADERS))
234+
metrics_json["exit_code"] = 4
235+
return metrics_json
236+
metrics_json["records_received"] = len(raw_data)
237+
# Creating a dataframe of the export csv first and then copying columns to match the database
238+
raw_data = raw_data.rename(columns={"DEFC_CODE": "DEFC", "DEFC_TITLE": "Public Law"})
239+
240+
group_model_df = group_model.to_pandas_df()
241+
group_mapping = group_model_df.groupby("group")["code"].agg(list).to_dict()
242+
243+
raw_data = apply_defc_derivations(raw_data, group_mapping)
244+
245+
raw_data = add_defc_outliers(raw_data, group_mapping)
246+
247+
# Clear any lingering np.nan's
248+
raw_data = raw_data.replace({np.nan: None})
249+
250+
logger.info("Checking for differences in DEFC data")
251+
defc_mapping = {
252+
"defc": "code",
253+
"public_law": "public_laws",
254+
"public_law_short_title": "public_law_short_titles",
255+
"group_name": "group",
256+
"urls": "urls",
257+
"is_valid": "is_valid",
258+
"earliest_public_law_enactment_date": "earliest_pl_action_date",
259+
}
260+
data = clean_data(raw_data, defc_mapping, {})
261+
diff_found = check_dataframe_diff(data, gold_model.to_pandas_df(), ["defc_id"], ["code"], date_format="%Y-%m-%d")
262+
if force_reload or diff_found:
263+
264+
# The only diff should be whenever a new code is added. Noting it here
265+
if diff_found:
266+
incoming_defcs = list(data["code"])
267+
curr_defcs = list(gold_model.to_pandas_df()["code"])
268+
diff_defcs = list(set(incoming_defcs) - set(curr_defcs))
269+
metrics_json["new_defc"] = diff_defcs
270+
logger.info(f"Difference found: {diff_defcs}")
271+
272+
logger.info("Overwriting new DEFC data to Broker")
273+
gold_model.save(data)
274+
275+
update_external_data_load_date(gold_model, start_time, datetime.now())
276+
logger.info("{} records inserted to DEFC".format(len(data)))
277+
278+
# convert the arrays to pipe-delimited strings
279+
defc_delim = "|"
280+
array_cols = ["Public Law", "Public Law Short Title", "URLs"]
281+
for array_col in array_cols:
282+
raw_data[array_col] = raw_data[array_col].apply(lambda value: defc_delim.join(value))
283+
284+
header_order = [
285+
"DEFC",
286+
"Public Law",
287+
"Public Law Short Title",
288+
"Group Name",
289+
"URLs",
290+
"Is Valid",
291+
"Earliest Public Law Enactment Date",
292+
]
293+
raw_data = raw_data[header_order]
294+
export_name = "def_codes.csv"
295+
logger.info("Exporting loaded DEFC file to {}".format(export_name))
296+
raw_data.to_csv(export_name, index=0)
297+
298+
s3.upload_file(export_name, CONFIG.PUBLIC_FILES_BUCKET, export_name)
299+
300+
os.remove(export_name)
301+
else:
302+
logger.info("No differences found, skipping defc table reload.")
305303

306-
total_defc_count = int_model.count()
304+
total_defc_count = gold_model.count()
307305

308306
metrics_json["total_defc_count"] = total_defc_count
309307

@@ -337,7 +335,7 @@ def setup_parser(parser: argparse.ArgumentParser) -> argparse.ArgumentParser:
337335

338336

339337
if __name__ == "__main__":
340-
parser = argparse.ArgumentParser(description="Process the bronze defc data into the silver defc table.")
338+
parser = argparse.ArgumentParser(description="Process the bronze defc data into the gold defc table.")
341339
parser = setup_parser(parser)
342340
args = parser.parse_args()
343341

brus_backend_common/tests/conftest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ def temp_file_path():
8989
# Should just be a simple csv creation, upload, and deletion per test
9090
@pytest.fixture(scope="function")
9191
def external_data_load_dates():
92-
raw_eld_model = LAKEHOUSE_MODELS["bronze.external_data_load_date"]()
92+
raw_eld_model = LAKEHOUSE_MODELS["gold.external_data_load_date"]()
9393
s3_client = _get_boto3("client", "s3")
9494

9595
raw_eld_model.initialize(recreate=True)

brus_backend_common/tests/integration/data/defc_groups.csv renamed to brus_backend_common/tests/integration/data/DEFC_MAPPING.csv

File renamed without changes.

brus_backend_common/tests/integration/loaders/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)