Skip to content

Commit bb16140

Browse files
authored
Merge pull request #1123 from CodeForPhilly/staging
Weekly PR from Staging to Main
2 parents 3feccde + a91c726 commit bb16140

File tree

12 files changed

+2821
-2301
lines changed

12 files changed

+2821
-2301
lines changed

.github/CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
@nlebovits @brandonfcohen1 @marvieqa @bacitracin @paulhchoi @CodeWritingCow
1+
@nlebovits @brandonfcohen1 @marvieqa @bacitracin @paulhchoi @CodeWritingCow @SofiaFasullo @cfreedman

.github/workflows/pr_checks_backend.yml

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,53 @@ jobs:
2929
with:
3030
python-version: '3.11.4'
3131

32+
test:
33+
runs-on: ubuntu-latest
34+
needs: setup
35+
defaults:
36+
run:
37+
working-directory: data/src
38+
env:
39+
VACANT_LOTS_DB: 'postgresql://postgres:${{ secrets.POSTGRES_PASSWORD }}@localhost:5433/vacantlotdb'
40+
services:
41+
postgres:
42+
image: postgis/postgis:16-3.4
43+
env:
44+
POSTGRES_USER: postgres
45+
POSTGRES_PASSWORD: ${{ secrets.POSTGRES_PASSWORD }}
46+
POSTGRES_DB: vacantlotdb
47+
ports:
48+
- 5433:5432
49+
# Set health checks to wait until postgres is ready
50+
options: >-
51+
--health-cmd pg_isready
52+
--health-interval 10s
53+
--health-timeout 5s
54+
--health-retries 5
55+
steps:
56+
- name: Checkout repository
57+
uses: actions/checkout@v4
58+
59+
- name: Set up Python
60+
uses: actions/setup-python@v5
61+
with:
62+
python-version: '3.11.4'
63+
64+
- name: Install and configure pipenv
65+
run: |
66+
python -m pip install --upgrade pip
67+
pip install pipenv
68+
echo "Using Python: $(which python)"
69+
pipenv --python $(which python) install --dev
70+
71+
- name: Install awkde
72+
working-directory: data/src/awkde
73+
run: pipenv run pip install .
74+
75+
- name: Run Pytest
76+
working-directory: data/src
77+
run: PYTHONPATH=$PYTHONPATH:. pipenv run pytest
78+
3279
run-formatter:
3380
runs-on: ubuntu-latest
3481
needs: setup

data/src/classes/backup_archive_database.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,17 +60,23 @@ def backup_schema(self):
6060
+ backup_schema_name
6161
+ ".spatial_ref_sys/public.spatial_ref_sys/' | psql -v ON_ERROR_STOP=1 "
6262
+ url
63-
+ " > /dev/null "
6463
)
6564
log.debug(mask_password(pgdump_command))
66-
complete_process = subprocess.run(pgdump_command, check=False, shell=True)
65+
complete_process = subprocess.run(
66+
pgdump_command,
67+
check=False,
68+
shell=True,
69+
stdout=subprocess.PIPE,
70+
stderr=subprocess.PIPE,
71+
text=True,
72+
)
6773

6874
if complete_process.returncode != 0 or complete_process.stderr:
6975
raise RuntimeError(
7076
"pg_dump command "
7177
+ mask_password(pgdump_command)
7278
+ " did not exit with success. "
73-
+ complete_process.stderr.decode()
79+
+ complete_process.stderr
7480
)
7581

7682
def archive_backup_schema(self):
@@ -123,4 +129,4 @@ def backup_tiles_file(self):
123129
bucket.copy_blob(blob,destination_bucket=bucket,new_name=backup_file_name)
124130
count += 1
125131
if count == 0:
126-
log.warning("No files were found to back up.")
132+
log.warning("No files were found to back up.")

data/src/classes/featurelayer.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,6 @@ def google_cloud_bucket() -> Bucket:
4242
return storage_client.bucket(bucket_name)
4343

4444

45-
bucket = google_cloud_bucket()
4645

4746

4847
class FeatureLayer:
@@ -61,6 +60,7 @@ def __init__(
6160
from_xy=False,
6261
use_wkb_geom_field=None,
6362
cols: list[str] = None,
63+
bucket: Bucket = None
6464
):
6565
self.name = name
6666
self.esri_rest_urls = (
@@ -77,6 +77,7 @@ def __init__(
7777
self.psql_table = name.lower().replace(" ", "_")
7878
self.input_crs = "EPSG:4326" if not from_xy else USE_CRS
7979
self.use_wkb_geom_field = use_wkb_geom_field
80+
self.bucket = bucket or google_cloud_bucket()
8081

8182
inputs = [self.esri_rest_urls, self.carto_sql_queries, self.gdf]
8283
non_none_inputs = [i for i in inputs if i is not None]
@@ -331,7 +332,7 @@ def build_and_publish(self, tiles_file_id_prefix: str) -> None:
331332
df_no_geom.to_parquet(temp_parquet)
332333

333334
# Upload Parquet to Google Cloud Storage
334-
blob_parquet = bucket.blob(f"{tiles_file_id_prefix}.parquet")
335+
blob_parquet = self.bucket.blob(f"{tiles_file_id_prefix}.parquet")
335336
try:
336337
blob_parquet.upload_from_filename(temp_parquet)
337338
parquet_size = os.stat(temp_parquet).st_size
@@ -400,7 +401,7 @@ def build_and_publish(self, tiles_file_id_prefix: str) -> None:
400401

401402
# Upload PMTiles to Google Cloud Storage
402403
for file in write_files:
403-
blob = bucket.blob(file)
404+
blob = self.bucket.blob(file)
404405
try:
405406
blob.upload_from_filename(temp_merged_pmtiles)
406407
print(f"PMTiles upload successful for {file}!")

data/src/test/conftest.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
from unittest.mock import MagicMock
2+
3+
import pytest
4+
from google.cloud.storage import Bucket
5+
6+
7+
@pytest.fixture(autouse=True)
8+
def mock_gcp_bucket(monkeypatch):
9+
mock_bucket = MagicMock(spec=Bucket)
10+
11+
monkeypatch.setattr("classes.featurelayer.google_cloud_bucket", lambda: mock_bucket)
12+
13+
return mock_bucket
14+
15+
16+
# Tell vulture this is used:
17+
_ = mock_gcp_bucket # Used indirectly by pytest

data/src/test/test_data_utils.py

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,63 @@
11
import unittest
22
import zipfile
33
from io import BytesIO
4-
from unittest.mock import MagicMock, patch
4+
from unittest.mock import MagicMock, Mock, patch
55

66
import geopandas as gpd
7-
from config.config import USE_CRS
87
from data_utils.park_priority import get_latest_shapefile_url, park_priority
98
from data_utils.ppr_properties import ppr_properties
109
from data_utils.vacant_properties import vacant_properties
1110
from shapely.geometry import Point
1211

12+
from config.config import USE_CRS
13+
1314

1415
class TestDataUtils(unittest.TestCase):
1516
"""
1617
Test methods for data utils feature layer classes
1718
"""
1819

20+
@classmethod
21+
def setUpClass(cls):
22+
# Create the mock GeoDataFrame that will be reused
23+
cls.mock_gdf = gpd.GeoDataFrame(
24+
{
25+
"ADDRESS": ["123 Main St"],
26+
"OWNER1": ["John Doe"],
27+
"OWNER2": ["Jane Doe"],
28+
"BLDG_DESC": ["House"],
29+
"CouncilDistrict": [1],
30+
"ZoningBaseDistrict": ["R1"],
31+
"ZipCode": ["19107"],
32+
"OPA_ID": ["12345"],
33+
"geometry": [Point(-75.1652, 39.9526)],
34+
},
35+
crs="EPSG:4326",
36+
)
37+
38+
def setUp(self):
39+
# Set up the mocks that will be used in each test
40+
self.patcher1 = patch("data_utils.vacant_properties.google_cloud_bucket")
41+
self.patcher2 = patch("geopandas.read_file")
42+
43+
self.mock_gcs = self.patcher1.start()
44+
self.mock_gpd = self.patcher2.start()
45+
46+
# Set up the mock chain
47+
mock_blob = Mock()
48+
mock_blob.exists.return_value = True
49+
mock_blob.download_as_bytes.return_value = b"dummy bytes"
50+
51+
mock_bucket = Mock()
52+
mock_bucket.blob.return_value = mock_blob
53+
54+
self.mock_gcs.return_value = mock_bucket
55+
self.mock_gpd.return_value = self.mock_gdf
56+
57+
def tearDown(self):
58+
self.patcher1.stop()
59+
self.patcher2.stop()
60+
1961
def test_get_latest_shapefile_url(self):
2062
"""
2163
Test the get_latest_shapefile_url function.
@@ -49,7 +91,7 @@ def test_get_latest_shapefile_url_mock(self, mock_get):
4991
def test_park_priority(
5092
self,
5193
mock_extract,
52-
mock_makedirs,
94+
_mock_makedirs,
5395
mock_exists,
5496
mock_to_file,
5597
mock_read_file,

data/src/test/test_diff_backup.py

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
1+
import os
12
from datetime import datetime
23

4+
import pytest
35
from classes.backup_archive_database import (
46
BackupArchiveDatabase,
57
backup_schema_name,
@@ -10,6 +12,10 @@
1012
from config.psql import conn, local_engine
1113
from sqlalchemy import inspect
1214

15+
pytestmark = pytest.mark.skip(
16+
reason="Skipping tests. The tests in test_diff_backup are designed for stateful, manual testing."
17+
)
18+
1319

1420
class TestDiffBackup:
1521
"""
@@ -60,26 +66,38 @@ def test_detail_report(self):
6066
url = diff.detail_report("vacant_properties")
6167
print(url)
6268

69+
@pytest.mark.skipif(
70+
not os.getenv("INTEGRATION_TESTING"),
71+
reason="For manual integration testing only. Export INTEGRATION_TESTING=True to run",
72+
)
6373
def test_upload_to_gcp(self):
6474
"""test a simple upload to Google cloud"""
6575
bucket = google_cloud_bucket()
6676
blob = bucket.blob("test.txt")
6777
blob.upload_from_string("test")
6878

79+
@pytest.mark.skipif(
80+
not os.getenv("INTEGRATION_TESTING"),
81+
reason="For manual integration testing only. Export INTEGRATION_TESTING=True to run",
82+
)
6983
def test_send_report_to_slack(self):
7084
"""CAREFUL: if configured, this will send a message to Slack, potentially our prod channel"""
7185
diff = DiffReport()
7286
diff.report = "This is the report"
7387
diff.send_report_to_slack()
7488

89+
@pytest.mark.skipif(
90+
not os.getenv("INTEGRATION_TESTING"),
91+
reason="For manual integration testing only. Export INTEGRATION_TESTING=True to run",
92+
)
7593
def test_email_report(self):
7694
"""CAREFUL: if configured, this will send email if configured"""
7795
diff = DiffReport()
7896
diff.report = "This is the report"
7997
diff.email_report()
8098

8199
def test_is_backup_schema_exists(self):
82-
"""test method for whether the backup schema exists """
100+
"""test method for whether the backup schema exists"""
83101
if TestDiffBackup.backup.is_backup_schema_exists():
84102
TestDiffBackup.backup.archive_backup_schema()
85103
conn.commit()
@@ -91,8 +109,6 @@ def test_is_backup_schema_exists(self):
91109
conn.commit()
92110
assert not TestDiffBackup.backup.is_backup_schema_exists()
93111

94-
95112
def test_backup_tiles_file(self):
96-
""" test backing up the tiles file """
113+
"""test backing up the tiles file"""
97114
TestDiffBackup.backup.backup_tiles_file()
98-

data/src/test/test_slack_error_reporter.py

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1+
import os
2+
import sys
13
import unittest
24
from unittest.mock import patch
35

4-
import sys
5-
import os
6-
76
sys.path.append(os.path.dirname(os.path.abspath(__file__)) + "/..")
87

98
from classes.slack_error_reporter import (
@@ -18,7 +17,7 @@ class TestSlackNotifier(unittest.TestCase):
1817
@patch(
1918
"classes.slack_error_reporter.os.getenv", return_value="mock_slack_token"
2019
) # Correct patching
21-
def test_send_error_to_slack(self, mock_getenv, mock_slack_post):
20+
def test_send_error_to_slack(self, _mock_getenv, mock_slack_post):
2221
"""Test that Slack error reporting is triggered correctly."""
2322

2423
error_message = "Test error message"
@@ -39,7 +38,7 @@ def test_send_error_to_slack(self, mock_getenv, mock_slack_post):
3938
@patch(
4039
"classes.slack_error_reporter.os.getenv", return_value=None
4140
) # Simulate missing Slack token
42-
def test_no_error_no_slack_message(self, mock_getenv, mock_slack_post):
41+
def test_no_error_no_slack_message(self, _mock_getenv, mock_slack_post):
4342
"""Test that Slack notification is not triggered if there's no error."""
4443

4544
# Call the Slack notification function (with no valid token)

0 commit comments

Comments
 (0)