diff --git a/apps/report-execution/pyproject.toml b/apps/report-execution/pyproject.toml index 393964825c..991df0a252 100644 --- a/apps/report-execution/pyproject.toml +++ b/apps/report-execution/pyproject.toml @@ -17,6 +17,7 @@ dev = [ "pytest>=9.0.2,<10.0.0", "pytest-mock>=3.15.1", "ruff>=0.15.0,<1.0.0", + "tablefaker>=1.8.0", "testcontainers>=4.14.1", ] diff --git a/apps/report-execution/pytest.toml b/apps/report-execution/pytest.toml index 031d3e55ac..d43640fe9c 100644 --- a/apps/report-execution/pytest.toml +++ b/apps/report-execution/pytest.toml @@ -10,4 +10,6 @@ addopts = [ "--strict-config", "--showlocals", "--import-mode=importlib" -] \ No newline at end of file +] +log_cli = true +log_cli_level = "INFO" diff --git a/apps/report-execution/src/db_transaction.py b/apps/report-execution/src/db_transaction.py index e61d6773bd..2ed736389a 100644 --- a/apps/report-execution/src/db_transaction.py +++ b/apps/report-execution/src/db_transaction.py @@ -9,25 +9,33 @@ class Transaction: """A database transaction abstraction for use in libraries.""" def __init__(self, cursor): - self.cursor = cursor + self._cursor = cursor - def execute(self, query: str, parameters: tuple = ()) -> Table: + def query(self, query: str, parameters: tuple = ()) -> Table: """Execute a query and have the data returned as a Table. DO NOT EXECUTE ANY PERMANENT CREATE, UPDATE, OR DELETE STATEMENTS Positional `?` placeholders can be used in the query and values passed as parameters in a tuple. - - If the query inserts or updates a temporary table, then the returned table - will be empty. """ - data = self.cursor.execute(query, parameters).fetchall() + data = self._cursor.execute(query, parameters).fetchall() columns = self._column_names() return Table(columns=columns, data=data) + def execute(self, query: str, parameters: tuple = ()) -> None: + """Execute a SQL statement and do not return any result. + + DO NOT EXECUTE ANY PERMANENT CREATE, UPDATE, OR DELETE STATEMENTS + + Positional `?` placeholders can be used in the query and values passed as + parameters in a tuple. + """ + self._cursor.execute(query, parameters) + return None + def _column_names(self) -> list[str]: - return [c[0] for c in self.cursor.description] + return [c[0] for c in self._cursor.description] @contextmanager @@ -40,3 +48,6 @@ def db_transaction(connection_string): with connection.cursor() as cursor: trx = Transaction(cursor) yield trx + + # not sure why this is needed - it shouldn't be per docs + connection.commit() diff --git a/apps/report-execution/src/libraries/nbs_custom.py b/apps/report-execution/src/libraries/nbs_custom.py index d28d163a2f..f0891da9d6 100644 --- a/apps/report-execution/src/libraries/nbs_custom.py +++ b/apps/report-execution/src/libraries/nbs_custom.py @@ -16,7 +16,7 @@ def execute( * Lifted the size check to a global check and refined the env var names * Date formatting still needs to be figured out """ - content = trx.execute(subset_query) + content = trx.query(subset_query) header = f'Custom Report For Table: {data_source_name}' subheader = None diff --git a/apps/report-execution/src/libraries/nbs_sr_05.py b/apps/report-execution/src/libraries/nbs_sr_05.py new file mode 100644 index 0000000000..9552c9b9ac --- /dev/null +++ b/apps/report-execution/src/libraries/nbs_sr_05.py @@ -0,0 +1,137 @@ +from src.db_transaction import Transaction +from src.models import ReportResult, TimeRange + + +def execute( + trx: Transaction, + subset_query: str, + data_source_name: str, + time_range: TimeRange | None = None, + **kwargs, +): + """Standard Report 05: Cases of Reportable Diseases for a specific state. + + Each row is a disease with columns for the: + * Current month total + * YTD total + * Prior YTD total + * 5 Year Median YTD total + * % change Current YTD vs 5 Year Median YTD + + Conversion notes: + * Export included the year as a column + * Export has columns in different order + """ + content = trx.query( + # State filtering is assumed to happen in the filters + f'WITH subset as ({subset_query})\n' + # base_data CTE + ', base_data as (' + 'SELECT phc_code_short_desc, MONTH(event_date) as month, ' + 'YEAR(event_date) as year, sum(group_case_cnt) as cases\n' + 'FROM subset\n' + 'AND event_date is not NULL\n' + 'AND DATEPART(dayofyear, event_date) <= ' + ' DATEPART(dayofyear, CURRENT_TIMESTAMP)\n' + 'AND YEAR(event_date) >= (YEAR(CURRENT_TIMESTAMP) - 5)\n' + 'GROUP BY phc_code_short_desc, MONTH(event_date), YEAR(event_date)\n' + ')\n' + # diseases CTE + ', diseases as (\n' + 'SELECT DISTINCT phc_code_short_desc\n' + 'FROM base_data\n' + ')\n' + # year_data CTE + ', year_data as (\n' + 'SELECT phc_code_short_desc, year, SUM(cases) as cases\n' + 'FROM base_data\n' + 'GROUP BY phc_code_short_desc, year' + ')\n' + # this_month CTE + ', this_month as (\n' + 'SELECT phc_code_short_desc, SUM(cases) as curr_month\n' + 'FROM base_data\n' + 'WHERE month = MONTH(CURRENT_TIMESTAMP)\n' + 'AND year = YEAR(CURRENT_TIMESTAMP)\n' + 'GROUP BY phc_code_short_desc' + ')\n' + # this_year CTE + ', this_year as (\n' + 'SELECT phc_code_short_desc, SUM(cases) as curr_ytd\n' + 'FROM year_data\n' + 'WHERE year = YEAR(CURRENT_TIMESTAMP)\n' + 'GROUP BY phc_code_short_desc' + ')\n' + # last_year CTE + ', last_year as (\n' + 'SELECT phc_code_short_desc, SUM(cases) as last_ytd\n' + 'FROM year_data\n' + 'WHERE year = (YEAR(CURRENT_TIMESTAMP) - 1)\n' + 'GROUP BY phc_code_short_desc' + ')\n' + # median_year CTE + ', median_year as (\n' + 'SELECT DISTINCT phc_code_short_desc, PERCENTILE_CONT(0.5) WITHIN GROUP ' + '(ORDER BY cases) OVER (PARTITION BY phc_code_short_desc) as median_ytd\n' + 'FROM year_data\n' + ')\n' + # Result select + 'SELECT d.phc_code_short_desc, COALESCE(curr_month, 0) as curr_month, \n' + 'COALESCE(curr_ytd, 0) as curr_ytd, COALESCE(last_ytd, 0) as last_ytd, \n' + 'COALESCE(median_ytd, 0) as median_ytd, \n' + 'IIF(' + ' COALESCE(median_ytd, 0) = 0, ' + ' 0, ' + ' COALESCE((curr_ytd - median_ytd) / median_ytd, 0)) as pct_chg\n' + 'FROM diseases d\n' + 'LEFT JOIN this_month tm on tm.phc_code_short_desc = d.phc_code_short_desc\n' + 'LEFT JOIN this_year ty on ty.phc_code_short_desc = d.phc_code_short_desc\n' + 'LEFT JOIN last_year ly on ly.phc_code_short_desc = d.phc_code_short_desc\n' + 'LEFT JOIN median_year my on my.phc_code_short_desc = d.phc_code_short_desc\n' + 'ORDER BY d.phc_code_short_desc asc' + ) + + # TODO: # noqa: FIX002 + # column header names + # sub header + + header = 'SR5: Cases of Reportable Diseases by State' + subheader = None + if time_range is not None: + subheader = f'{time_range.start} - {time_range.end}' + + description = ( + '*Data Source:* nbs_ods.PHCDemographic (publichealthcasefact)\n' + '*Output:* Report demonstrates, in table form, the total number of ' + 'Investigation(s) [both Individual and Summary] irrespective of Case Status.\n' + 'Output:\n' + '* Does not include Investigation(s) that have been logically deleted\n' + '* Is filtered based on the state, disease(s) and advanced criteria selected ' + 'by user\n' + '* Will not include Investigation(s) that do not have a value for the State ' + 'selected by the user\n' + '* Is based on month and year of the calculated Event Date\n' + '*Calculations:*' + '* *Current Month Totals by disease:* Total Investigation(s) [both Individual ' + 'and Summary] where the Year and Month of the Event Date equal the current ' + 'Year and Month\n' + '* *Current Year Totals by disease:* Total Investigation(s) [both Individual ' + 'and Summary] where the Year of the Event Date equal the current Year\n' + '* *Previous Year Totals by disease:* Total Investigation(s) [both ' + 'Individual and Summary] where the Year of the Event Date equal last Year\n' + '* *5-Year median:* Median number of Investigation(s) [both Individual and ' + 'Summary] for the past five years\n' + '* *Percentage change (current year vs. 5 year median):* Percentage change ' + 'between the Current Year Totals by disease and the 5-Year median\n' + ' * *Event Date:* Derived using the hierarchy of Onset Date, Diagnosis Date, ' + 'Report to County, Report to State and Date the Investigation was created in ' + 'the NBS.\n' + ) + + return ReportResult( + content_type='table', + content=content, + header=header, + subheader=subheader, + description=description, + ) diff --git a/apps/report-execution/tests/conftest.py b/apps/report-execution/tests/conftest.py index 2833582193..ce6452c009 100644 --- a/apps/report-execution/tests/conftest.py +++ b/apps/report-execution/tests/conftest.py @@ -3,8 +3,11 @@ from contextlib import contextmanager import pytest -from testcontainers.compose import DockerCompose +import tablefaker +from testcontainers.compose import ContainerIsNotRunning, DockerCompose +from src import utils +from src.db_transaction import db_transaction from src.models import Table @@ -81,6 +84,7 @@ def setup_containers(request): """Set up DB and report execution containers.""" logging.info('Setting up containers tests...') compose_path = os.path.join(os.path.dirname(__file__), '../../../cdc-sandbox') + services = ['report-execution', 'nbs-mssql'] compose_file_names = [ 'docker-compose.yml', '../apps/report-execution/tests/integration/docker-compose.yml', @@ -88,20 +92,112 @@ def setup_containers(request): containers = DockerCompose( compose_path, compose_file_name=compose_file_names, - services=['report-execution', 'nbs-mssql'], - build=True, + services=services, env_file=['../sample.env', '../apps/report-execution/sample.env'], + build=True, ) report_exec_url = 'http://0.0.0.0:8001/status' + def maybe_get_container(name): + try: + containers.get_container(name) + except ContainerIsNotRunning: + return None + + containers_to_stop = [ + maybe_get_container(service) + for service in services + if maybe_get_container(service) is not None + ] + containers.start() containers.wait_for(report_exec_url) logging.info('Ingestion ready to test!') def teardown(): logging.info('Service logs...\n') - logging.info(containers.get_logs()) logging.info('Tests finished! Tearing down.') - containers.stop() + for container in containers_to_stop: + container.stop() request.addfinalizer(teardown) + + +def get_faker_sql(schema_name: str) -> str: + """Process a fakertable schema and return the sql as a string.""" + faker_path = os.path.join( + os.path.dirname(__file__), + 'integration', + 'assets', + 'tablefaker_schema', + schema_name, + ) + target_file_path = os.path.join(os.path.dirname(__file__), 'fake.sql') + tablefaker.to_sql(faker_path, target_file_path=target_file_path) + with open(target_file_path) as f: + result = f.read() + + os.remove(target_file_path) + + # KLUDGE: NULL writing is not always correct + result = result.replace(' nan,', ' NULL,') + result = result.replace(' nan)', ' NULL)') + result = result.replace(' ,', ' NULL,') + result = result.replace(' )', ' NULL)') + return result + + +def temp_name(table_name: str) -> str: + """Assumes `[schema].[dbo].[table name]` format. + + Not using temp tables as the usage spans connections. + """ + return table_name[0:-1] + '_temp]' + + +@pytest.fixture(scope='class') +def fake_db_table(request): + """Replace a DB table with fake table per the tablefaker schema.""" + db_table = request.module.db_table + fk_tables = getattr(request.module, 'db_fk_tables', []) + faker_schema = request.module.faker_schema + faker_sql = get_faker_sql(faker_schema) + + conn_string = utils.get_env_or_error('DATABASE_CONN_STRING') + + # swap out original data for fake data + with db_transaction(conn_string) as trx: + # Tables with foreign keys pointing to the table we want to replace need to + # be backed up and cleared out to avoid FK constraint violations + for fk_table in fk_tables: + temp_fk_table = temp_name(fk_table) + trx.execute( + f"IF OBJECT_ID('{temp_fk_table}') IS NOT NULL " + f'DROP TABLE {temp_fk_table}' + ) + trx.execute(f'SELECT * INTO {temp_fk_table} FROM {fk_table}') + trx.execute(f'DELETE {fk_table}') + logging.info(f'cleared FK table: {fk_table}') + + temp_db_table = temp_name(db_table) + trx.execute( + f"IF OBJECT_ID('{temp_db_table}') IS NOT NULL DROP TABLE {temp_db_table}" + ) + trx.execute(f'SELECT * INTO {temp_db_table} FROM {db_table}') + trx.execute(f'DELETE {db_table}') + logging.info(f'cleared table: {db_table}') + trx.execute(faker_sql) + logging.info(f'Inserted fake data: {db_table}') + + # avoid connection inside connection + yield + + # restore the original data + with db_transaction(conn_string) as trx: + trx.execute(f'DELETE {db_table}') + trx.execute(f'INSERT INTO {db_table} SELECT * FROM {temp_db_table}') + logging.info(f'Restored table: {db_table}') + + for fk_table in fk_tables: + trx.execute(f'INSERT INTO {fk_table} SELECT * FROM {temp_name(fk_table)}') + logging.info(f'Restored FK table: {fk_table}') diff --git a/apps/report-execution/tests/integration/assets/tablefaker_schema/phc_demographic.yaml b/apps/report-execution/tests/integration/assets/tablefaker_schema/phc_demographic.yaml new file mode 100644 index 0000000000..3a0d0da82e --- /dev/null +++ b/apps/report-execution/tests/integration/assets/tablefaker_schema/phc_demographic.yaml @@ -0,0 +1,202 @@ +config: + seed: 4 + +tables: + # Table backing PHCDemographic view + - table_name: "[NBS_ODSE].[dbo].[PublicHealthCaseFact]" + row_count: 1000 + columns: + # required columns: + - column_name: public_health_case_uid + data: row_id + is_primary_key: true + - column_name: case_class_cd + data: random.choice(["N", "C", ""]) + - column_name: PHC_code + data: random.choice(["10190", "10140", "502651"]) + - column_name: PHC_code_desc + data: | + if PHC_code == "10190": + return "Pertussis" + elif PHC_code == "10140": + return "Measles" + elif PHC_code == "502651": + return "Salmonellosis" + - column_name: prog_area_cd + data: | + if PHC_code in ("10190", "10140"): + return "GCD" + elif PHC_code == "502651": + return "FOOD" + + # Case basics + - column_name: PHC_code_short_desc + type: string + data: PHC_code_desc + - column_name: event_date + data: fake.date_between(start_date="-6y", end_date="+1w") + null_percentage: 0.05 + - column_name: group_case_cnt + data: fake.random_int(1, 90) + null_percentage: 0.05 + + # Location + - column_name: state + type: string + data: random.choice(["Georgia", "Tennessee"]) if random.random() < .95 else None + - column_name: state_cd + type: string + data: | + if state == "Georgia": + return "13" + elif state == "Tennessee": + return "47" + else: + return None + +# === Columns not needed for reports yet === +# adults_in_house_nbr smallint NULL, +# ageInMonths smallint NULL, +# ageInYears smallint NULL, +# age_category_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# age_reported_time datetime NULL, +# age_reported_unit_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# age_reported numeric(8,0) NULL, +# awareness_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# awareness_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# birth_gender_cd char(1) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# birth_order_nbr smallint NULL, +# birth_time datetime NULL, +# birth_time_calc datetime NULL, +# birth_time_std datetime NULL, +# case_type_cd char(1) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# cd_system_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# cd_system_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# census_block_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# census_minor_civil_division_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# census_track_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# cnty_code_desc_txt varchar(200) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# children_in_house_nbr smallint NULL, +# city_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# city_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# confidentiality_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# confidentiality_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# confirmation_method_cd varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# confirmation_method_time datetime NULL, +# county varchar(255) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# cntry_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# cnty_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# curr_sex_cd char(1) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# deceased_ind_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# deceased_time datetime NULL, +# detection_method_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# detection_method_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# diagnosis_date datetime NULL, +# disease_imported_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# disease_imported_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# education_level_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# ELP_class_cd varchar(10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# ELP_from_time datetime NULL, +# ELP_to_time datetime NULL, +# ethnic_group_ind varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# ethnic_group_ind_desc varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# event_type varchar(10) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# education_level_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# firstNotificationSenddate datetime NULL, +# firstNotificationdate datetime NULL, +# firstNotificationStatus varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# firstNotificationSubmittedBy bigint NULL, +# geoLatitude real NULL, +# geoLongitude real NULL, +# investigation_status_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# investigatorAssigneddate datetime NULL, +# investigatorName varchar(102) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# investigatorPhone varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# jurisdiction_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# lastNotificationdate datetime NULL, +# lastNotificationSenddate datetime NULL, +# lastNotificationSubmittedBy bigint NULL, +# marital_status_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# marital_status_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# mart_record_creation_date datetime NULL, +# mart_record_creation_time datetime NULL, +# mmwr_week numeric(8,0) NULL, +# mmwr_year numeric(8,0) NULL, +# MSA_congress_district_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# multiple_birth_ind varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# notifCreatedCount int NULL, +# notificationdate datetime NULL, +# notifSentCount int NULL, +# occupation_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# onSetDate datetime NULL, +# organizationName varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# outcome_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# outbreak_from_time datetime NULL, +# outbreak_ind varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# outbreak_name varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# outbreak_to_time datetime NULL, +# PAR_type_cd varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# pat_age_at_onset numeric(8,0) NULL, +# pat_age_at_onset_unit_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# postal_locator_uid bigint NULL, +# person_cd varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# person_code_desc varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# person_uid bigint NULL, +# PHC_add_time datetime NULL, +# PHC_code_short_desc varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# prim_lang_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# prim_lang_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# providerPhone varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# providerName varchar(102) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# PST_record_status_time datetime NULL, +# PST_record_status_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# race_concatenated_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# race_concatenated_desc_txt varchar(500) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# region_district_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# record_status_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# reporterName varchar(102) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# reporterPhone varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# rpt_cnty_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# rpt_form_cmplt_time datetime NULL, +# rpt_source_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# rpt_source_desc_txt varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# rpt_to_county_time datetime NULL, +# rpt_to_state_time datetime NULL, +# shared_ind varchar(1) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# state_code_short_desc_txt varchar(200) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# status_cd char(1) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# street_addr1 varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# street_addr2 varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# ELP_use_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# zip_cd varchar(20) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# patientName varchar(102) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# jurisdiction varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# investigationstartdate datetime NULL, +# program_jurisdiction_oid bigint NULL, +# report_date datetime NULL, +# person_parent_uid bigint NULL, +# person_local_id varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# sub_addr_as_of_date datetime NULL, +# state_case_id varchar(199) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# LOCAL_ID varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# NOTIFCURRENTSTATE varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# age_reported_unit_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# birth_gender_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# case_class_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# cntry_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# curr_sex_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# investigation_status_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# occupation_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# outcome_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# pat_age_at_onset_unit_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# prog_area_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# rpt_cnty_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# outbreak_name_desc varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# confirmation_method_desc_txt varchar(300) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# LASTUPDATE datetime NULL, +# PHCTXT varchar(2000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# NOTITXT varchar(2000) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# NOTIFICATION_LOCAL_ID varchar(50) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, +# HSPTL_ADMISSION_DT datetime NULL, +# HSPTL_DISCHARGE_DT datetime NULL, +# hospitalized_ind varchar(100) COLLATE SQL_Latin1_General_CP1_CI_AS NULL, diff --git a/apps/report-execution/tests/integration/libraries/nbs_sr_05.py b/apps/report-execution/tests/integration/libraries/nbs_sr_05.py new file mode 100644 index 0000000000..9682b2211e --- /dev/null +++ b/apps/report-execution/tests/integration/libraries/nbs_sr_05.py @@ -0,0 +1,54 @@ +import logging + +import pytest + +from src.execute_report import execute_report +from src.models import ReportSpec + +db_table = '[NBS_ODSE].[dbo].[PublicHealthCaseFact]' +db_fk_tables = ['[NBS_ODSE].[dbo].[SubjectRaceInfo]'] +faker_schema = 'phc_demographic.yaml' + + +@pytest.mark.usefixtures('setup_containers', 'fake_db_table') +@pytest.mark.integration +class TestIntegrationNbsSr05Library: + """Integration tests for the nbs_custom library.""" + + def test_execute_report_with_time_range(self): + report_spec = ReportSpec.model_validate( + { + 'version': 1, + 'is_export': True, + 'is_builtin': True, + 'report_title': 'NBS Custom', + 'library_name': 'nbs_sr_05', + # Filter operator is used here as it is a stable, small table + 'data_source_name': '[NBS_ODSE].[dbo].[PHCDemographic]', + 'subset_query': 'SELECT * FROM [NBS_ODSE].[dbo].[PHCDemographic]', + 'time_range': {'start': '2024-01-01', 'end': '2024-12-31'}, + } + ) + + result = execute_report(report_spec) + assert result.header == ('SR5: Cases of Reportable Diseases by State') + assert result.subheader == '2024-01-01 - 2024-12-31' + assert result.content_type == 'table' + logging.info(result.content.data) + + assert len(result.content.data) >= 1 + assert len(result.content.data[0]) == 4 + assert len(result.content.data[0]) == len(result.content.columns) + + record = None + for row in result.content.data: + if ( + row[0] == 'Georgia' + and row[1] == 'Gwinnett County' + and row[2] == 'Pertussis' + ): + record = row + break + + assert record is not None + assert record[3] >= 1 diff --git a/apps/report-execution/tests/integration/main.py b/apps/report-execution/tests/integration/main.py index f5bb6beadd..22338e181f 100644 --- a/apps/report-execution/tests/integration/main.py +++ b/apps/report-execution/tests/integration/main.py @@ -36,6 +36,6 @@ def test_report_runs(self): result = json.loads(response.read()) assert ( - result['description'] + result['header'] == 'Custom Report For Table: [NBS_ODSE].[dbo].[Filter_code]' ) diff --git a/apps/report-execution/uv.lock b/apps/report-execution/uv.lock index 255784bfc0..f6efbb5625 100644 --- a/apps/report-execution/uv.lock +++ b/apps/report-execution/uv.lock @@ -154,6 +154,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "cramjam" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/14/12/34bf6e840a79130dfd0da7badfb6f7810b8fcfd60e75b0539372667b41b6/cramjam-2.11.0.tar.gz", hash = "sha256:5c82500ed91605c2d9781380b378397012e25127e89d64f460fea6aeac4389b4", size = 99100, upload-time = "2025-07-27T21:25:07.559Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/07/a1051cdbbe6d723df16d756b97f09da7c1adb69e29695c58f0392bc12515/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:7ba5e38c9fbd06f086f4a5a64a1a5b7b417cd3f8fc07a20e5c03651f72f36100", size = 3554141, upload-time = "2025-07-27T21:23:17.938Z" }, + { url = "https://files.pythonhosted.org/packages/74/66/58487d2e16ef3d04f51a7c7f0e69823e806744b4c21101e89da4873074bc/cramjam-2.11.0-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b8adeee57b41fe08e4520698a4b0bd3cc76dbd81f99424b806d70a5256a391d3", size = 1860353, upload-time = "2025-07-27T21:23:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/67/b4/67f6254d166ffbcc9d5fa1b56876eaa920c32ebc8e9d3d525b27296b693b/cramjam-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b96a74fa03a636c8a7d76f700d50e9a8bc17a516d6a72d28711225d641e30968", size = 1693832, upload-time = "2025-07-27T21:23:21.185Z" }, + { url = "https://files.pythonhosted.org/packages/55/a3/4e0b31c0d454ae70c04684ed7c13d3c67b4c31790c278c1e788cb804fa4a/cramjam-2.11.0-cp314-cp314-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c3811a56fa32e00b377ef79121c0193311fd7501f0fb378f254c7f083cc1fbe0", size = 2027080, upload-time = "2025-07-27T21:23:23.303Z" }, + { url = "https://files.pythonhosted.org/packages/d9/c7/5e8eed361d1d3b8be14f38a54852c5370cc0ceb2c2d543b8ba590c34f080/cramjam-2.11.0-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c5d927e87461f8a0d448e4ab5eb2bca9f31ca5d8ea86d70c6f470bb5bc666d7e", size = 1761543, upload-time = "2025-07-27T21:23:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/09/0c/06b7f8b0ce9fde89470505116a01fc0b6cb92d406c4fb1e46f168b5d3fa5/cramjam-2.11.0-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f1f5c450121430fd89cb5767e0a9728ecc65997768fd4027d069cb0368af62f9", size = 1854636, upload-time = "2025-07-27T21:23:26.987Z" }, + { url = "https://files.pythonhosted.org/packages/6f/c6/6ebc02c9d5acdf4e5f2b1ec6e1252bd5feee25762246798ae823b3347457/cramjam-2.11.0-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:724aa7490be50235d97f07e2ca10067927c5d7f336b786ddbc868470e822aa25", size = 2032715, upload-time = "2025-07-27T21:23:28.603Z" }, + { url = "https://files.pythonhosted.org/packages/a2/77/a122971c23f5ca4b53e4322c647ac7554626c95978f92d19419315dddd05/cramjam-2.11.0-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:54c4637122e7cfd7aac5c1d3d4c02364f446d6923ea34cf9d0e8816d6e7a4936", size = 2069039, upload-time = "2025-07-27T21:23:30.319Z" }, + { url = "https://files.pythonhosted.org/packages/19/0f/f6121b90b86b9093c066889274d26a1de3f29969d45c2ed1ecbe2033cb78/cramjam-2.11.0-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:17eb39b1696179fb471eea2de958fa21f40a2cd8bf6b40d428312d5541e19dc4", size = 1979566, upload-time = "2025-07-27T21:23:32.002Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/f95bc57fd7f4166ce6da816cfa917fb7df4bb80e669eb459d85586498414/cramjam-2.11.0-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:36aa5a798aa34e11813a80425a30d8e052d8de4a28f27bfc0368cfc454d1b403", size = 2030905, upload-time = "2025-07-27T21:23:33.696Z" }, + { url = "https://files.pythonhosted.org/packages/fc/52/e429de4e8bc86ee65e090dae0f87f45abd271742c63fb2d03c522ffde28a/cramjam-2.11.0-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:449fca52774dc0199545fbf11f5128933e5a6833946707885cf7be8018017839", size = 2155592, upload-time = "2025-07-27T21:23:35.375Z" }, + { url = "https://files.pythonhosted.org/packages/6c/6c/65a7a0207787ad39ad804af4da7f06a60149de19481d73d270b540657234/cramjam-2.11.0-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:d87d37b3d476f4f7623c56a232045d25bd9b988314702ea01bd9b4a94948a778", size = 2170839, upload-time = "2025-07-27T21:23:37.197Z" }, + { url = "https://files.pythonhosted.org/packages/b2/c5/5c5db505ba692bc844246b066e23901d5905a32baf2f33719c620e65887f/cramjam-2.11.0-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:26cb45c47d71982d76282e303931c6dd4baee1753e5d48f9a89b3a63e690b3a3", size = 2157236, upload-time = "2025-07-27T21:23:38.854Z" }, + { url = "https://files.pythonhosted.org/packages/b0/22/88e6693e60afe98901e5bbe91b8dea193e3aa7f42e2770f9c3339f5c1065/cramjam-2.11.0-cp314-cp314-win32.whl", hash = "sha256:4efe919d443c2fd112fe25fe636a52f9628250c9a50d9bddb0488d8a6c09acc6", size = 1604136, upload-time = "2025-07-27T21:23:40.56Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f8/01618801cd59ccedcc99f0f96d20be67d8cfc3497da9ccaaad6b481781dd/cramjam-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:ccec3524ea41b9abd5600e3e27001fd774199dbb4f7b9cb248fcee37d4bda84c", size = 1710272, upload-time = "2025-07-27T21:23:42.236Z" }, + { url = "https://files.pythonhosted.org/packages/40/81/6cdb3ed222d13ae86bda77aafe8d50566e81a1169d49ed195b6263610704/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:966ac9358b23d21ecd895c418c048e806fd254e46d09b1ff0cdad2eba195ea3e", size = 3559671, upload-time = "2025-07-27T21:23:44.504Z" }, + { url = "https://files.pythonhosted.org/packages/cb/43/52b7e54fe5ba1ef0270d9fdc43dabd7971f70ea2d7179be918c997820247/cramjam-2.11.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:387f09d647a0d38dcb4539f8a14281f8eb6bb1d3e023471eb18a5974b2121c86", size = 1867876, upload-time = "2025-07-27T21:23:46.987Z" }, + { url = "https://files.pythonhosted.org/packages/9d/28/30d5b8d10acd30db3193bc562a313bff722888eaa45cfe32aa09389f2b24/cramjam-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:665b0d8fbbb1a7f300265b43926457ec78385200133e41fef19d85790fc1e800", size = 1695562, upload-time = "2025-07-27T21:23:48.644Z" }, + { url = "https://files.pythonhosted.org/packages/d9/86/ec806f986e01b896a650655024ea52a13e25c3ac8a3a382f493089483cdc/cramjam-2.11.0-cp314-cp314t-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:ca905387c7a371531b9622d93471be4d745ef715f2890c3702479cd4fc85aa51", size = 2025056, upload-time = "2025-07-27T21:23:50.404Z" }, + { url = "https://files.pythonhosted.org/packages/09/43/c2c17586b90848d29d63181f7d14b8bd3a7d00975ad46e3edf2af8af7e1f/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c1aa56aef2c8af55a21ed39040a94a12b53fb23beea290f94d19a76027e2ffb", size = 1764084, upload-time = "2025-07-27T21:23:52.265Z" }, + { url = "https://files.pythonhosted.org/packages/2b/a9/68bc334fadb434a61df10071dc8606702aa4f5b6cdb2df62474fc21d2845/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e5db59c1cdfaa2ab85cc988e602d6919495f735ca8a5fd7603608eb1e23c26d5", size = 1854859, upload-time = "2025-07-27T21:23:54.085Z" }, + { url = "https://files.pythonhosted.org/packages/5b/4e/b48e67835b5811ec5e9cb2e2bcba9c3fd76dab3e732569fe801b542c6ca9/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b1f893014f00fe5e89a660a032e813bf9f6d91de74cd1490cdb13b2b59d0c9a3", size = 2035970, upload-time = "2025-07-27T21:23:55.758Z" }, + { url = "https://files.pythonhosted.org/packages/c4/70/d2ac33d572b4d90f7f0f2c8a1d60fb48f06b128fdc2c05f9b49891bb0279/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c26a1eb487947010f5de24943bd7c422dad955b2b0f8650762539778c380ca89", size = 2069320, upload-time = "2025-07-27T21:23:57.494Z" }, + { url = "https://files.pythonhosted.org/packages/1d/4c/85cec77af4a74308ba5fca8e296c4e2f80ec465c537afc7ab1e0ca2f9a00/cramjam-2.11.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d5c8bfb438d94e7b892d1426da5fc4b4a5370cc360df9b8d9d77c33b896c37e", size = 1982668, upload-time = "2025-07-27T21:23:59.126Z" }, + { url = "https://files.pythonhosted.org/packages/55/45/938546d1629e008cc3138df7c424ef892719b1796ff408a2ab8550032e5e/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:cb1fb8c9337ab0da25a01c05d69a0463209c347f16512ac43be5986f3d1ebaf4", size = 2034028, upload-time = "2025-07-27T21:24:00.865Z" }, + { url = "https://files.pythonhosted.org/packages/01/76/b5a53e20505555f1640e66dcf70394bcf51a1a3a072aa18ea35135a0f9ed/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:1f6449f6de52dde3e2f1038284910c8765a397a25e2d05083870f3f5e7fc682c", size = 2155513, upload-time = "2025-07-27T21:24:02.92Z" }, + { url = "https://files.pythonhosted.org/packages/84/12/8d3f6ceefae81bbe45a347fdfa2219d9f3ac75ebc304f92cd5fcb4fbddc5/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_i686.whl", hash = "sha256:382dec4f996be48ed9c6958d4e30c2b89435d7c2c4dbf32480b3b8886293dd65", size = 2170035, upload-time = "2025-07-27T21:24:04.558Z" }, + { url = "https://files.pythonhosted.org/packages/4b/85/3be6f0a1398f976070672be64f61895f8839857618a2d8cc0d3ab529d3dc/cramjam-2.11.0-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:d388bd5723732c3afe1dd1d181e4213cc4e1be210b080572e7d5749f6e955656", size = 2160229, upload-time = "2025-07-27T21:24:06.729Z" }, + { url = "https://files.pythonhosted.org/packages/57/5e/66cfc3635511b20014bbb3f2ecf0095efb3049e9e96a4a9e478e4f3d7b78/cramjam-2.11.0-cp314-cp314t-win32.whl", hash = "sha256:0a70ff17f8e1d13f322df616505550f0f4c39eda62290acb56f069d4857037c8", size = 1610267, upload-time = "2025-07-27T21:24:08.428Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c6/c71e82e041c95ffe6a92ac707785500aa2a515a4339c2c7dd67e3c449249/cramjam-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:028400d699442d40dbda02f74158c73d05cb76587a12490d0bfedd958fd49188", size = 1713108, upload-time = "2025-07-27T21:24:10.147Z" }, +] + [[package]] name = "cryptography" version = "46.0.5" @@ -243,6 +281,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604, upload-time = "2025-08-26T13:09:05.858Z" }, ] +[[package]] +name = "et-xmlfile" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/38/af70d7ab1ae9d4da450eeec1fa3918940a5fafb9055e934af8d6eb0c2313/et_xmlfile-2.0.0.tar.gz", hash = "sha256:dab3f4764309081ce75662649be815c4c9081e88f0837825f90fd28317d4da54", size = 17234, upload-time = "2024-10-25T17:25:40.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, +] + +[[package]] +name = "faker" +version = "40.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "tzdata", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/03/14428edc541467c460d363f6e94bee9acc271f3e62470630fc9a647d0cf2/faker-40.8.0.tar.gz", hash = "sha256:936a3c9be6c004433f20aa4d99095df5dec82b8c7ad07459756041f8c1728875", size = 1956493, upload-time = "2026-03-04T16:18:48.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/3b/c6348f1e285e75b069085b18110a4e6325b763a5d35d5e204356fc7c20b3/faker-40.8.0-py3-none-any.whl", hash = "sha256:eb21bdba18f7a8375382eb94fb436fce07046893dc94cb20817d28deb0c3d579", size = 1989124, upload-time = "2026-03-04T16:18:46.45Z" }, +] + [[package]] name = "fastapi" version = "0.135.1" @@ -348,6 +407,46 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/85/11/0aa8455af26f0ae89e42be67f3a874255ee5d7f0f026fc86e8d56f76b428/fastar-0.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e59673307b6a08210987059a2bdea2614fe26e3335d0e5d1a3d95f49a05b1418", size = 460467, upload-time = "2025-11-26T02:36:07.978Z" }, ] +[[package]] +name = "fastparquet" +version = "2025.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cramjam" }, + { name = "fsspec" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1e/ad/87f7f5750685e8e0a359d732c85332481ba9b5723af579f8755f81154d0b/fastparquet-2025.12.0.tar.gz", hash = "sha256:85f807d3846c7691855a68ed7ff6ee40654b72b997f5b1199e6310a1e19d1cd5", size = 480045, upload-time = "2025-12-18T16:22:22.016Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/3a/7bc677df8d4dadc4f7f2dee035c9578aa0e79e2c0f58ddc78e197e24fbc2/fastparquet-2025.12.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:c0fe3f8a73160be7778e1a54ac4463b49a7e35e1f6c7fb9876b36d2ec572bead", size = 900184, upload-time = "2025-12-18T21:53:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/c5/aa/2c726bfd2a6c0e18854a924c3faeee1c2e934b03915c8d2111a3c3f7c0fd/fastparquet-2025.12.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:aec3a736e3c43f7d8f911946f4c56b8cc17e803932ca0cb75bb2643796adabeb", size = 692174, upload-time = "2025-12-18T21:54:16.329Z" }, + { url = "https://files.pythonhosted.org/packages/e3/c4/a0936ac68c7209ab4979ac45ab59d6efa700b5ddac62031f4ddd6b462f0d/fastparquet-2025.12.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8aa32817dd571b10974b04c66e470a181208840466f155280ff3df43946c6b92", size = 1755044, upload-time = "2025-12-18T21:58:18.404Z" }, + { url = "https://files.pythonhosted.org/packages/64/54/0b06b3c8a778fd0795426e2a529672cb6925541ba2a1076e3d8940a6c565/fastparquet-2025.12.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9f5a9dc0036838950e449d6d05dd48e25b6b2741568b4e0872823195e23890b1", size = 1793074, upload-time = "2025-12-18T21:57:34.995Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/7b5109f7ec39dbe3dc847a3a3d63105a78717d9fe874abbba7a90f047b31/fastparquet-2025.12.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:05971c0974b5bb00c01622fe248f83008e58f06224212c778f7d46ccb092a7d2", size = 1802137, upload-time = "2025-12-18T21:58:50.504Z" }, + { url = "https://files.pythonhosted.org/packages/6a/8b/f3acc13ffec64803bbbb56977147e8ea105426f5034c9041d5d6d01c7e62/fastparquet-2025.12.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:e86a3407933ff510dad077139eaae2c664d2bdeeb0b6ece2a1e1c98c87257dd3", size = 1781629, upload-time = "2025-12-18T21:58:20.015Z" }, + { url = "https://files.pythonhosted.org/packages/13/66/c102a8b01976afd4408ccfc7f121516168faaafb86a201716116ce5120d0/fastparquet-2025.12.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:00349200d1103a34e34a94f535c1bf19870ab1654388b8a2aa50ca34046fc071", size = 1806721, upload-time = "2025-12-18T21:58:52.495Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/13340110f7daa99db2c9f090a2790602515dabc6dc263e88931482aaaf66/fastparquet-2025.12.0-cp314-cp314-win_amd64.whl", hash = "sha256:8f42036889a5729da1cae6e2a599b9c8b93af6f99973015ac14225d529300982", size = 673274, upload-time = "2025-12-18T21:59:13.642Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/22f149b01de42cc69a4faa1047e1902a91bf1085e79ccba20caceded8607/fastparquet-2025.12.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a4e9165c98f0fdac70aba728055424b0b2830a9cb02e9048d3d82d2e9c0294c1", size = 929604, upload-time = "2025-12-18T21:53:57.814Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e8/18b0831254eb8a3b07caf374a23dc011eeffa5f8bc5507d2b43498bc577d/fastparquet-2025.12.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:69b80faf4c9d154fc95d3f291a55b1d782c684e9fcfe443a274c3e92d36a963c", size = 708902, upload-time = "2025-12-18T21:54:17.803Z" }, + { url = "https://files.pythonhosted.org/packages/e8/0c/a29aa2c84b46d35e5dc4ece79f0fca67a6889a51ac3d0330a7fb22cf82fd/fastparquet-2025.12.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:8b9c9108127778d9628cce342f4e4c98890a4b686f677ed4973bc0edd6e25af9", size = 1771639, upload-time = "2025-12-18T21:58:21.761Z" }, + { url = "https://files.pythonhosted.org/packages/9f/62/2d851d5effe3c95b36ae948fb7da46d00ae8f88ae0d6907403b2ac5183c9/fastparquet-2025.12.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c052cacccfc6f8cb2ca98e809380969214b79471d49867f802184d3ea68d1e9", size = 1830649, upload-time = "2025-12-18T21:57:36.884Z" }, + { url = "https://files.pythonhosted.org/packages/bf/a1/868f2d5db3fc9965e4ca6a68f6ab5fef3ade0104136e3556299c952bc720/fastparquet-2025.12.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c027278b5372e11a005b8d1ad9d85e86a9d70077dc8918cda99f90e657dc7251", size = 1820867, upload-time = "2025-12-18T21:58:54.645Z" }, + { url = "https://files.pythonhosted.org/packages/20/9c/f900734e546425509cf1f5cc9cd4f75275dff45c40d8c65feb0f148e4118/fastparquet-2025.12.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:618cc4388f5bc1d85587c0842f6c0d1af8ab2e27a5aa8074aa233b157f68f2c0", size = 1786865, upload-time = "2025-12-18T21:58:23.136Z" }, + { url = "https://files.pythonhosted.org/packages/34/14/88068907d837964d407d5835df6672ea635881d6e0937ca21dac088342bc/fastparquet-2025.12.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3e3fac9215a00a6a6836400437a7797841cb2f6393e38ff0a77c5e1aa37cfa44", size = 1817440, upload-time = "2025-12-18T21:58:56.702Z" }, + { url = "https://files.pythonhosted.org/packages/7a/d9/5c4a0871d7b111c7115c02feb071c07a0a1c1da0afc1c35d9acb7958fd95/fastparquet-2025.12.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1bbacfff213b1cfbfa189ba1023f3fa9e3025ce6590c1becdb76a6ac1e84e623", size = 707783, upload-time = "2025-12-18T21:59:15.138Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + [[package]] name = "h11" version = "0.16.0" @@ -553,6 +652,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/32/0a/2ec5deea6dcd158f254a7b372fb09cfba5719419c8d66343bab35237b3fb/numpy-2.4.2-cp314-cp314t-win_arm64.whl", hash = "sha256:1f92f53998a17265194018d1cc321b2e96e900ca52d54c7c77837b71b9465181", size = 10565379, upload-time = "2026-01-31T23:12:51.345Z" }, ] +[[package]] +name = "openpyxl" +version = "3.1.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "et-xmlfile" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3d/f9/88d94a75de065ea32619465d2f77b29a0469500e99012523b91cc4141cd1/openpyxl-3.1.5.tar.gz", hash = "sha256:cf0e3cf56142039133628b5acffe8ef0c12bc902d2aadd3e0fe5878dc08d1050", size = 186464, upload-time = "2024-06-28T14:03:44.161Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/da/977ded879c29cbd04de313843e76868e6e13408a94ed6b987245dc7c8506/openpyxl-3.1.5-py2.py3-none-any.whl", hash = "sha256:5282c12b107bffeef825f4617dc029afaf41d0ea60823bbb665ef3079dc79de2", size = 250910, upload-time = "2024-06-28T14:03:41.161Z" }, +] + [[package]] name = "packaging" version = "26.0" @@ -600,6 +711,28 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + [[package]] name = "pycparser" version = "3.0" @@ -845,6 +978,7 @@ dev = [ { name = "pytest" }, { name = "pytest-mock" }, { name = "ruff" }, + { name = "tablefaker" }, { name = "testcontainers" }, ] @@ -862,6 +996,7 @@ dev = [ { name = "pytest", specifier = ">=9.0.2,<10.0.0" }, { name = "pytest-mock", specifier = ">=3.15.1" }, { name = "ruff", specifier = ">=0.15.0,<1.0.0" }, + { name = "tablefaker", specifier = ">=1.8.0" }, { name = "testcontainers", specifier = ">=4.14.1" }, ] @@ -1013,6 +1148,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/0d/13d1d239a25cbfb19e740db83143e95c772a1fe10202dda4b76792b114dd/starlette-0.52.1-py3-none-any.whl", hash = "sha256:0029d43eb3d273bc4f83a08720b4912ea4b071087a3b48db01b7c839f7954d74", size = 74272, upload-time = "2026-01-18T13:34:09.188Z" }, ] +[[package]] +name = "tablefaker" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "faker" }, + { name = "fastparquet" }, + { name = "openpyxl" }, + { name = "pandas" }, + { name = "psutil" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/12/26/fe0d1936c6c437dd3f8f81fa3658302aaf95c1bdb8fd66b88add0bcc4945/tablefaker-1.8.0.tar.gz", hash = "sha256:f6b74dfc45a42af3c8986512ffb77e50174188cd388992590b3813f00c805e3c", size = 51589, upload-time = "2025-10-10T19:06:16.069Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/8a/4958c6a3bc47b618c0b7f5d470ce58af53ed33a350e32e02f91c44d6aeb6/tablefaker-1.8.0-py3-none-any.whl", hash = "sha256:901c23b003b13c7ba05d21e42807121b34aedc9e9781fda45aebaf51207e539e", size = 41188, upload-time = "2025-10-10T19:06:14.71Z" }, +] + [[package]] name = "testcontainers" version = "4.14.1"