Skip to content

Commit 10a1776

Browse files
BFD-4192: Configure additional CI checks for Python projects (#2758)
1 parent ee90f4a commit 10a1776

47 files changed

Lines changed: 1641 additions & 1422 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ on:
66
- "**/*.py"
77
- "**/*requirement*.txt"
88
- "**/uv.lock"
9+
- ".github/workflows/ci-python.yml"
910

1011
merge_group:
1112

@@ -24,8 +25,27 @@ jobs:
2425
projectName: "sftp-outbound-transfer-lambda",
2526
projectRoot: "ops/terraform/services/eft/lambda_src/sftp_outbound_transfer",
2627
testsRoot: "ops/terraform/services/eft/lambda_src/sftp_outbound_transfer/tests",
28+
uvCommand: "uv sync",
29+
pytestCommand: "uv run pytest",
2730
pythonVersion: "3.13",
2831
},
32+
{
33+
projectName: "locust",
34+
projectRoot: "apps/utils/locust_tests",
35+
testsRoot: "apps/utils/locust_tests",
36+
uvCommand: "uv sync --group lambda-run-locust",
37+
# no tests
38+
pytestCommand: "echo 'no tests'",
39+
pythonVersion: "3.13"
40+
},
41+
{
42+
projectName: "idr-pipeline",
43+
projectRoot: "apps/bfd-pipeline/bfd-pipeline-idr",
44+
testsRoot: "apps/bfd-pipeline/bfd-pipeline-idr",
45+
uvCommand: "uv sync",
46+
pytestCommand: "uv run pytest",
47+
pythonVersion: "3.13"
48+
}
2949
]
3050
fail-fast: false
3151
steps:
@@ -45,5 +65,12 @@ jobs:
4565
echo "Testing ${{ matrix.projects.projectName }}"
4666
cd ${{ github.workspace }}/${{ matrix.projects.projectRoot }}
4767
uv python install
48-
uv sync
49-
uv run pytest ${{ github.workspace }}/${{ matrix.projects.testsRoot }}
68+
${{ matrix.projects.uvCommand }}
69+
# check linting
70+
uv run ruff check
71+
# check formatting
72+
uv run ruff format --check
73+
# check types
74+
uv run pyright .
75+
# tests
76+
${{ matrix.projects.pytestCommand }} ${{ github.workspace }}/${{ matrix.projects.testsRoot }}

apps/bfd-pipeline/bfd-pipeline-idr/extractor.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,8 @@ def extract_idr_data(self, cls: type[T], progress: LoadProgress | None) -> Itera
7979
"{WHERE_CLAUSE}",
8080
f"""
8181
WHERE
82-
({update_timestamp_col} IS NOT NULL AND {batch_timestamp_col} {op} %(timestamp)s {update_clause})
82+
({update_timestamp_col} IS NOT NULL
83+
AND {batch_timestamp_col} {op} %(timestamp)s {update_clause})
8384
AND {batch_timestamp_col} {op} '{get_min_transaction_date()}'
8485
""",
8586
).replace("{ORDER_BY}", f"ORDER BY {batch_timestamp_col}"),
@@ -133,7 +134,8 @@ def extract_many(self, cls: type[T], sql: str, params: dict[str, DbType]) -> Ite
133134
cursor_execute_timer.stop()
134135

135136
cursor_fetch_timer.start()
136-
# fetchmany can return list[dict] or list[tuple] but we'll only use queries that return dicts
137+
# fetchmany can return list[dict] or list[tuple] but we'll only use
138+
# queries that return dicts
137139
batch: list[dict[str, DbType]] = cur.fetchmany(self.batch_size) # type: ignore[assignment]
138140
cursor_fetch_timer.stop()
139141

apps/bfd-pipeline/bfd-pipeline-idr/loader.py

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ def print_timers() -> None:
2424
def get_connection_string() -> str:
2525
port = os.environ.get("BFD_DB_PORT") or "5432"
2626
dbname = os.environ.get("BFD_DB_NAME") or "idr"
27-
return f"host={os.environ['BFD_DB_ENDPOINT']} port={port} dbname={dbname} user={os.environ['BFD_DB_USERNAME']} password={os.environ['BFD_DB_PASSWORD']}"
27+
return f"host={os.environ['BFD_DB_ENDPOINT']} port={port} dbname={dbname} \
28+
user={os.environ['BFD_DB_USERNAME']} password={os.environ['BFD_DB_PASSWORD']}"
2829

2930

3031
class PostgresLoader:
@@ -52,17 +53,21 @@ def load(
5253
update_set = ", ".join([f"{v}=EXCLUDED.{v}" for v in insert_cols if v not in unique_key])
5354
timestamp = datetime.now(UTC)
5455
table = model.table()
55-
# trim the schema from the table name to create the temp table (temp tables can't be created with an explicit schema set)
56+
# trim the schema from the table name to create the temp table
57+
# (temp tables can't be created with an explicit schema set)
5658
temp_table = table.split(".")[1] + "_temp"
5759
with self.conn.cursor() as cur:
5860
# load each batch in a separate transaction
5961
for results in fetch_results:
6062
# Load each batch into a temp table
61-
# This is necessary because we want to use COPY to quickly transfer everything into Postgres
62-
# but COPY can't handle constraint conflicts natively.
63+
# This is necessary because we want to use COPY to quickly
64+
# transfer everything into Postgres, but COPY can't handle
65+
# constraint conflicts natively.
66+
#
6367
# Note that temp tables don't use WAL so that helps with throughput as well.
6468
#
65-
# For simplicity's sake, we'll create our temp tables using the existing schema and just drop the columns we need to ignore
69+
# For simplicity's sake, we'll create our temp tables using the existing schema and
70+
# just drop the columns we need to ignore.
6671
temp_table_timer.start()
6772
cur.execute(
6873
f"CREATE TEMPORARY TABLE {temp_table} (LIKE {table}) ON COMMIT DROP" # type: ignore
@@ -78,12 +83,13 @@ def load(
7883
temp_table_timer.stop()
7984

8085
# Use COPY to load the batch into Postgres.
81-
# COPY has a number of optimizations that make bulk loading more efficient than a bunch of INSERTs.
82-
# The entire operation is performed in a single statement, resulting in fewer network round-trips,
83-
# less WAL activity, and less context switching.
86+
# COPY has a number of optimizations that make bulk loading more efficient
87+
# than a bunch of INSERTs.
88+
# The entire operation is performed in a single statement, resulting in
89+
# fewer network round-trips, less WAL activity, and less context switching.
8490

85-
# Even though we need to move the data from the temp table in the next step, it should still be
86-
# faster than alternatives.
91+
# Even though we need to move the data from the temp table in the next step,
92+
# it should still be faster than alternatives.
8793
copy_timer.start()
8894
with cur.copy(f"COPY {temp_table} ({cols_str}) FROM STDIN") as copy: # type: ignore
8995
for row in results:
@@ -92,8 +98,10 @@ def load(
9298
copy_timer.stop()
9399

94100
if len(results) > 0:
95-
# For immutable tables, we may still be attempting to re-load some data due to a batch cancellation.
96-
# In these cases, we can assume any conflicting rows have already been loaded so "DO NOTHING" is appropriate here.
101+
# For immutable tables, we may still be attempting to re-load some data
102+
# due to a batch cancellation.
103+
# In these cases, we can assume any conflicting rows have already been loaded so
104+
# "DO NOTHING" is appropriate here.
97105
on_conflict = (
98106
"DO NOTHING"
99107
if immutable
@@ -113,7 +121,8 @@ def load(
113121
insert_timer.stop()
114122

115123
last = results[len(results) - 1].model_dump()
116-
# Some tables that contain reference data (like contract info) may not have the normal IDR timestamps
124+
# Some tables that contain reference data (like contract info) may not have the
125+
# normal IDR timestamps.
117126
# For now we won't support incremental refreshes for those tables
118127
batch_timestamp_col = model.batch_timestamp_col(
119128
progress is None or progress.is_historical()

apps/bfd-pipeline/bfd-pipeline-idr/model.py

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from collections.abc import Iterable
22
from datetime import UTC, date, datetime
3-
from typing import Annotated, Optional, TypeVar
3+
from typing import Annotated, TypeVar
44

55
from pydantic import BaseModel, BeforeValidator
66

@@ -92,7 +92,7 @@ def batch_timestamp_col(cls, is_historical: bool) -> str | None:
9292
return cls._get_timestamp_col(BATCH_TIMESTAMP)
9393

9494
@classmethod
95-
def batch_timestamp_col_alias(cls, is_historical: bool) -> Optional[str]:
95+
def batch_timestamp_col_alias(cls, is_historical: bool) -> str | None:
9696
col = cls.batch_timestamp_col(is_historical)
9797
if col:
9898
return cls._format_column_alias(col)
@@ -377,7 +377,7 @@ class IdrBeneficiaryXref(IdrBaseModel):
377377
src_rec_ctre_ts: Annotated[datetime, {PRIMARY_KEY: True, BATCH_TIMESTAMP: True}]
378378

379379
@staticmethod
380-
def table():
380+
def table() -> str:
381381
return "idr.beneficiary_xref"
382382

383383
@staticmethod
@@ -408,10 +408,12 @@ def table() -> str:
408408

409409
@staticmethod
410410
def _current_fetch_query() -> str:
411-
# equivalent to "select distinct on", but Snowflake has different syntax for that so it's unfortunately not portable
411+
# equivalent to "select distinct on", but Snowflake has different syntax for that,
412+
# so it's unfortunately not portable
412413
return """
413414
WITH dupes as (
414-
SELECT {COLUMNS}, ROW_NUMBER() OVER (PARTITION BY bene_sk, cntrct_pbp_sk, bene_enrlmt_efctv_dt
415+
SELECT {COLUMNS}, ROW_NUMBER() OVER (
416+
PARTITION BY bene_sk, cntrct_pbp_sk, bene_enrlmt_efctv_dt
415417
{ORDER_BY} DESC) as row_order
416418
FROM cms_vdm_view_mdcr_prd.v2_mdcr_bene_elctn_prd_usg
417419
{WHERE_CLAUSE}
@@ -722,7 +724,8 @@ def table() -> str:
722724
def _current_fetch_query() -> str:
723725
clm = ALIAS_CLM
724726
line = ALIAS_LINE
725-
# Note: joining on clm_uniq_id isn't strictly necessary, but it did seem to improve performance a bit
727+
# Note: joining on clm_uniq_id isn't strictly necessary,
728+
# but it did seem to improve performance a bit
726729
return f"""
727730
SELECT {{COLUMNS}}
728731
FROM cms_vdm_view_mdcr_prd.v2_mdcr_clm {clm}
@@ -741,8 +744,7 @@ def _current_fetch_query() -> str:
741744
def transform_default_hipps_code(value: str | None) -> str:
742745
if value is None or value == "00000":
743746
return ""
744-
else:
745-
return value
747+
return value
746748

747749

748750
class IdrClaimLineInstitutional(IdrBaseModel):

apps/bfd-pipeline/bfd-pipeline-idr/pyproject.toml

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,13 @@ dependencies = [
1111
]
1212

1313
[dependency-groups]
14-
dev = ["ruff>=0.11.11", "uv>=0.7.8", "pytest>=8.3.5", "testcontainers>=4.9.2"]
14+
dev = [
15+
"ruff>=0.11.11",
16+
"uv>=0.7.8",
17+
"pytest>=8.3.5",
18+
"testcontainers>=4.9.2",
19+
"pyright>=1.1.403",
20+
]
1521

1622
# [[tool.mypy.overrides]]
1723
# module = ["testcontainers.*"]
@@ -53,6 +59,8 @@ select = [
5359
"FURB",
5460
"RUF",
5561
]
62+
# Don't require docstrings
63+
ignore = ["D100", "D101", "D102", "D103", "D104", "D105", "D107"]
5664

5765
[tool.ruff.lint.pydocstyle]
5866
convention = "pep257"

apps/bfd-pipeline/bfd-pipeline-idr/test_pipeline.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import time
2-
from datetime import datetime, timezone, UTC
2+
from collections.abc import Generator
3+
from datetime import UTC, datetime
4+
from pathlib import Path
35
from typing import cast
46

57
import psycopg
@@ -20,14 +22,16 @@
2022

2123

2224
@pytest.fixture(scope="session", autouse=True)
23-
def psql_url():
25+
def psql_url() -> Generator[str]:
2426
with PostgresContainer("postgres:16", driver="") as postgres:
2527
psql_url = postgres.get_connection_url()
2628
conn = psycopg.connect(psql_url)
2729

28-
conn.execute(open("./mock-idr.sql", "r").read()) # type: ignore
30+
with Path("./mock-idr.sql").open() as f:
31+
conn.execute(f.read()) # type: ignore
2932
conn.commit()
30-
conn.execute(open("./bfd.sql", "r").read()) # type: ignore
33+
with Path("./bfd.sql").open() as f:
34+
conn.execute(f.read()) # type: ignore
3135
conn.commit()
3236

3337
load_from_csv(conn, "./test_samples1")
@@ -36,7 +40,7 @@ def psql_url():
3640

3741

3842
class TestPipeline:
39-
def test_pipeline(self, psql_url: str):
43+
def test_pipeline(self, psql_url: str) -> None:
4044
run_pipeline(PostgresExtractor(psql_url, 100_000), psql_url)
4145
conn = cast(psycopg.Connection[DictRow], psycopg.connect(psql_url, row_factory=dict_row)) # type: ignore
4246
cur = conn.execute("select * from idr.beneficiary order by bene_sk")

apps/bfd-pipeline/bfd-pipeline-idr/timer.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,16 @@
22

33

44
class Timer:
5-
def __init__(self, name: str):
5+
def __init__(self, name: str) -> None:
66
self.elapsed = 0.0
77
self.perf_start = 0.0
88
self.name = name
99

10-
def start(self):
10+
def start(self) -> None:
1111
self.perf_start = time.perf_counter()
1212

13-
def stop(self):
13+
def stop(self) -> None:
1414
self.elapsed += time.perf_counter() - self.perf_start
1515

16-
def print_results(self):
16+
def print_results(self) -> None:
1717
print(f"Time taken for {self.name}: {self.elapsed:.6f} seconds")

apps/bfd-pipeline/bfd-pipeline-idr/uv.lock

Lines changed: 24 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)