Skip to content

Commit 7b128dd

Browse files
authored
Merge pull request #532 from DataIntegrationGroup/transfer-fix-review-feedback
Transfer fix review feedback
2 parents db0dc8f + 52a94ec commit 7b128dd

6 files changed

Lines changed: 237 additions & 147 deletions

File tree

.github/workflows/tests.yml

Lines changed: 75 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,9 @@ permissions:
1111
contents: read
1212

1313
jobs:
14-
run-tests:
14+
unit-tests:
1515
runs-on: ubuntu-latest
1616

17-
# Set shared env vars ONCE here for all steps
1817
env:
1918
MODE: development
2019
POSTGRES_HOST: localhost
@@ -56,12 +55,21 @@ jobs:
5655
uses: astral-sh/setup-uv@v5
5756
with:
5857
enable-cache: true
58+
cache-dependency-glob: uv.lock
5959

6060
- name: Set up Python
61+
id: setup-python
6162
uses: actions/setup-python@v6.2.0
6263
with:
6364
python-version-file: "pyproject.toml"
6465

66+
- name: Cache project virtualenv
67+
id: cache-venv
68+
uses: actions/cache@v4
69+
with:
70+
path: .venv
71+
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('uv.lock') }}
72+
6573
- name: Install the project
6674
run: uv sync --locked --all-extras --dev
6775

@@ -76,13 +84,74 @@ jobs:
7684
- name: Run tests
7785
run: uv run pytest -vv --durations=20 --cov --cov-report=xml --junitxml=junit.xml --ignore=tests/transfers
7886

79-
- name: Run BDD tests
80-
run: |
81-
uv run behave tests/features --tags="@backend and @production and not @skip" --no-capture
82-
8387
- name: Upload results to Codecov
8488
uses: codecov/codecov-action@v5
8589
with:
8690
report_type: test_results
8791
token: ${{ secrets.CODECOV_TOKEN }}
8892

93+
bdd-tests:
94+
runs-on: ubuntu-latest
95+
96+
env:
97+
MODE: development
98+
POSTGRES_HOST: localhost
99+
POSTGRES_PORT: 5432
100+
POSTGRES_USER: postgres
101+
POSTGRES_PASSWORD: postgres
102+
POSTGRES_DB: ocotilloapi_test
103+
DB_DRIVER: postgres
104+
BASE_URL: http://localhost:8000
105+
SESSION_SECRET_KEY: supersecretkeyforunittests
106+
AUTHENTIK_DISABLE_AUTHENTICATION: 1
107+
108+
services:
109+
postgis:
110+
image: postgis/postgis:17-3.5
111+
env:
112+
POSTGRES_PASSWORD: postgres
113+
POSTGRES_PORT: 5432
114+
options: >-
115+
--health-cmd pg_isready
116+
--health-interval 10s
117+
--health-timeout 5s
118+
--health-retries 5
119+
ports:
120+
- 5432:5432
121+
122+
steps:
123+
- name: Check out source repository
124+
uses: actions/checkout@v6.0.2
125+
126+
- name: Install uv
127+
uses: astral-sh/setup-uv@v5
128+
with:
129+
enable-cache: true
130+
cache-dependency-glob: uv.lock
131+
132+
- name: Set up Python
133+
id: setup-python
134+
uses: actions/setup-python@v6.2.0
135+
with:
136+
python-version-file: "pyproject.toml"
137+
138+
- name: Cache project virtualenv
139+
id: cache-venv
140+
uses: actions/cache@v4
141+
with:
142+
path: .venv
143+
key: venv-${{ runner.os }}-${{ steps.setup-python.outputs.python-version }}-${{ hashFiles('uv.lock') }}
144+
145+
- name: Install the project
146+
run: uv sync --locked --all-extras --dev
147+
148+
- name: Show Alembic heads
149+
run: uv run alembic heads
150+
151+
- name: Create test database
152+
run: |
153+
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -c "CREATE DATABASE ocotilloapi_test"
154+
PGPASSWORD=postgres psql -h localhost -p 5432 -U postgres -d ocotilloapi_test -c "CREATE EXTENSION IF NOT EXISTS postgis"
155+
156+
- name: Run BDD tests
157+
run: uv run behave tests/features --tags="@backend and @production and not @skip" --no-capture

core/lexicon.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2260,7 +2260,12 @@
22602260
"categories": ["status_value"],
22612261
"term": "Open",
22622262
"definition": "The well is open."
2263-
},
2263+
},
2264+
{
2265+
"categories": ["status_value"],
2266+
"term": "Open (unequipped)",
2267+
"definition": "The well is open and unequipped."
2268+
},
22642269
{
22652270
"categories": ["status_value"],
22662271
"term": "Closed",

tests/features/environment.py

Lines changed: 24 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,13 @@
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
1515
# =============== ================================================================
16+
import os
1617
import random
1718
from datetime import datetime, timedelta
1819

20+
from alembic import command
21+
from alembic.config import Config
22+
from core.initializers import init_lexicon, init_parameter
1923
from db import (
2024
Location,
2125
Thing,
@@ -40,15 +44,14 @@
4044
ThingAquiferAssociation,
4145
GeologicFormation,
4246
ThingGeologicFormationAssociation,
43-
Base,
4447
Asset,
4548
Contact,
4649
Sample,
50+
Base,
4751
)
4852
from db.engine import session_ctx
49-
from services.util import get_bool_env
53+
from db.initialization import recreate_public_schema, sync_search_vector_triggers
5054
from sqlalchemy import select
51-
from transfers.transfer import _drop_and_rebuild_db
5255

5356

5457
def add_context_object_container(name):
@@ -499,24 +502,26 @@ def add_geologic_formation(context, session, formation_code, well):
499502
return formation
500503

501504

502-
def before_all(context):
503-
context.objects = {}
505+
def _alembic_config() -> Config:
506+
root = os.path.dirname(os.path.dirname(os.path.dirname(__file__)))
507+
cfg = Config(os.path.join(root, "alembic.ini"))
508+
cfg.set_main_option("script_location", os.path.join(root, "alembic"))
509+
return cfg
504510

505-
rebuild_raw = get_bool_env("DROP_AND_REBUILD_DB")
506-
rebuild = rebuild_raw if isinstance(rebuild_raw, bool) else False
507-
erase_data = False
508-
if rebuild:
509-
_drop_and_rebuild_db()
510-
elif erase_data:
511-
with session_ctx() as session:
512-
for table in reversed(Base.metadata.sorted_tables):
513-
if table.name in ("alembic_version", "parameter"):
514-
continue
515-
elif table.name.startswith("lexicon"):
516-
continue
517511

518-
session.execute(table.delete())
519-
session.commit()
512+
def _initialize_test_schema() -> None:
513+
with session_ctx() as session:
514+
recreate_public_schema(session)
515+
command.upgrade(_alembic_config(), "head")
516+
with session_ctx() as session:
517+
sync_search_vector_triggers(session)
518+
init_lexicon()
519+
init_parameter()
520+
521+
522+
def before_all(context):
523+
context.objects = {}
524+
_initialize_test_schema()
520525

521526
with session_ctx() as session:
522527

tests/test_transfer_legacy_dates.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,12 @@
2424
import datetime
2525
from unittest.mock import patch
2626

27+
import numpy as np
2728
import pandas as pd
2829
import pytest
2930

3031
from db import Sample
32+
from transfers.well_transfer import _normalize_completion_date
3133
from transfers.util import make_location
3234
from transfers.waterlevels_transfer import WaterLevelTransferer
3335

@@ -207,6 +209,37 @@ def test_make_observation_maps_data_quality():
207209
assert observation.nma_data_quality == "Mapped Quality"
208210

209211

212+
def test_normalize_completion_date_drops_time_from_datetime():
213+
value = datetime.datetime(2024, 7, 3, 14, 15, 16)
214+
normalized, parse_failed = _normalize_completion_date(value)
215+
assert normalized == datetime.date(2024, 7, 3)
216+
assert parse_failed is False
217+
218+
219+
def test_normalize_completion_date_drops_time_from_timestamp_and_string():
220+
ts_value = pd.Timestamp("2021-05-06 23:59:00")
221+
str_value = "2021-05-06 23:59:00.000"
222+
normalized_ts, parse_failed_ts = _normalize_completion_date(ts_value)
223+
normalized_str, parse_failed_str = _normalize_completion_date(str_value)
224+
assert normalized_ts == datetime.date(2021, 5, 6)
225+
assert normalized_str == datetime.date(2021, 5, 6)
226+
assert parse_failed_ts is False
227+
assert parse_failed_str is False
228+
229+
230+
def test_normalize_completion_date_handles_numpy_datetime64():
231+
value = np.datetime64("2020-01-02T03:04:05")
232+
normalized, parse_failed = _normalize_completion_date(value)
233+
assert normalized == datetime.date(2020, 1, 2)
234+
assert parse_failed is False
235+
236+
237+
def test_normalize_completion_date_invalid_returns_none_and_parse_failed():
238+
normalized, parse_failed = _normalize_completion_date("not-a-date")
239+
assert normalized is None
240+
assert parse_failed is True
241+
242+
210243
def test_get_dt_utc_respects_time_datum():
211244
transfer = WaterLevelTransferer.__new__(WaterLevelTransferer)
212245
transfer.errors = []

transfers/util.py

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,38 @@
5757
}
5858

5959

60+
DEFINED_RECORDING_INTERVALS = {
61+
"SA-0174": (1, "hour"),
62+
"SO-0140": (15, "minute"),
63+
"SO-0145": (15, "minute"),
64+
"SO-0146": (15, "minute"),
65+
"SO-0148": (15, "minute"),
66+
"SO-0160": (15, "minute"),
67+
"SO-0163": (15, "minute"),
68+
"SO-0165": (15, "minute"),
69+
"SO-0166": (15, "minute"),
70+
"SO-0175": (15, "minute"),
71+
"SO-0177": (15, "minute"),
72+
"SO-0189": (15, "minute"),
73+
"SO-0191": (15, "minute"),
74+
"SO-0194": (15, "minute"),
75+
"SO-0200": (15, "minute"),
76+
"SO-0204": (15, "minute"),
77+
"SO-0224": (15, "minute"),
78+
"SO-0238": (15, "minute"),
79+
"SO-0247": (15, "minute"),
80+
"SO-0249": (15, "minute"),
81+
"SO-0261": (15, "minute"),
82+
"SM-0055": (6, "hour"),
83+
"SM-0259": (12, "hour"),
84+
"HS-038": (12, "hour"),
85+
"EB-220": (12, "hour"),
86+
"SO-0144": (15, "minute"),
87+
"SO-0142": (15, "minute"),
88+
"SO-0190": (15, "minute"),
89+
}
90+
91+
6092
class MeasuringPointEstimator:
6193
def __init__(self):
6294
df = read_csv("WaterLevels")
@@ -123,6 +155,12 @@ def estimate_measuring_point_height(
123155
return mphs, mph_descs, start_dates, end_dates
124156

125157

158+
def _get_defined_recording_interval(pointid: str) -> tuple[int, str] | None:
159+
if pointid in DEFINED_RECORDING_INTERVALS:
160+
return DEFINED_RECORDING_INTERVALS[pointid]
161+
return None
162+
163+
126164
class SensorParameterEstimator:
127165
def __init__(self, sensor_type: str):
128166
if sensor_type == "Pressure Transducer":
@@ -156,7 +194,16 @@ def estimate_recording_interval(
156194
installation_date: datetime = None,
157195
removal_date: datetime = None,
158196
) -> tuple[int | None, str | None, str | None]:
197+
"""
198+
return estimated recording interval, unit, and error message if applicable
199+
"""
159200
point_id = record.PointID
201+
202+
# get statically defined recording interval provided by Ethan
203+
ri = _get_defined_recording_interval(point_id)
204+
if ri is not None:
205+
return ri[0], ri[1], None
206+
160207
cdf = self._get_values(point_id)
161208
if len(cdf) == 0:
162209
return None, None, f"No measurements found for PointID: {point_id}"

0 commit comments

Comments
 (0)