Skip to content

Commit

Permalink
SingleLocation tables - Use a datetime throughout
Browse files Browse the repository at this point in the history
Use a datetime column for the Site and PointData for consistency. This
replaces the separate time and date column for site and combines them
into one. Also adds the hybrid attribute date to the Site object to
be able to only query for dates.

Both table now carry the `datetime` column to indicate this behavior.

PR-feedback
  • Loading branch information
jomey committed Oct 18, 2024
1 parent 5d211b1 commit e87c564
Show file tree
Hide file tree
Showing 10 changed files with 77 additions and 50 deletions.
13 changes: 3 additions & 10 deletions snowexsql/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,13 +118,6 @@ def _filter_measurement_type(cls, qry, value):
def _filter_doi(cls, qry, value):
return qry.join(cls.MODEL.doi).filter(DOI.doi == value)

@classmethod
def _filter_column(cls, query_model, key):
if key == 'date' and query_model == PointData:
return PointData.date_only
else:
return getattr(query_model, key)

@classmethod
def extend_qry(cls, qry, check_size=True, **kwargs):
if cls.MODEL is None:
Expand Down Expand Up @@ -164,12 +157,12 @@ def extend_qry(cls, qry, check_size=True, **kwargs):
if "_greater_equal" in k:
key = k.split("_greater_equal")[0]
qry = qry.filter(
cls._filter_column(qry_model, key) >= v
getattr(qry_model, key) >= v
)
elif "_less_equal" in k:
key = k.split("_less_equal")[0]
qry = qry.filter(
cls._filter_column(qry_model, key) <= v
getattr(qry_model, key) <= v
)
# Filter linked columns
elif k == "instrument":
Expand All @@ -189,7 +182,7 @@ def extend_qry(cls, qry, check_size=True, **kwargs):
# Filter to exact value
else:
qry = qry.filter(
cls._filter_column(qry_model, k) == v
getattr(qry_model, k) == v
)
LOG.debug(
f"Filtering {k} to list {v}"
Expand Down
15 changes: 6 additions & 9 deletions snowexsql/tables/point_data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from sqlalchemy import Column, Date, DateTime, Float, Integer, String
from sqlalchemy import Column, Date, Float, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property

from .base import Base
Expand All @@ -14,9 +14,6 @@ class PointData(Base, SingleLocationData, HasPointObservation):
"""
__tablename__ = 'points'

# Date of the measurement with time
date = Column(DateTime)

version_number = Column(Integer)
equipment = Column(String())
value = Column(Float)
Expand All @@ -25,15 +22,15 @@ class PointData(Base, SingleLocationData, HasPointObservation):
units = Column(String())

@hybrid_property
def date_only(self):
def date(self):
"""
Helper attribute to only query for dates of measurements
"""
return self.date.date()
return self.datetime.date()

@date_only.expression
def date_only(cls):
@date.expression
def date(cls):
"""
Helper attribute to only query for dates of measurements
"""
return cls.date.cast(Date)
return cls.datetime.cast(Date)
5 changes: 3 additions & 2 deletions snowexsql/tables/single_location.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
from geoalchemy2 import Geometry
from sqlalchemy import Column, Float, Time
from sqlalchemy import Column, DateTime, Float


class SingleLocationData:
"""
Base class for point and layer data
"""
time = Column(Time(timezone=True))
# Date of the measurement with time
datetime = Column(DateTime(timezone=True))
elevation = Column(Float)
geom = Column(Geometry("POINT"))
22 changes: 18 additions & 4 deletions snowexsql/tables/site.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
from sqlalchemy import Column, String, Date, Float, Integer, ForeignKey
from sqlalchemy.orm import Mapped, relationship, mapped_column
from typing import List

from sqlalchemy import Column, Date, Float, ForeignKey, Integer, String
from sqlalchemy.ext.hybrid import hybrid_property
from sqlalchemy.orm import Mapped, mapped_column, relationship

from .base import Base
from .campaign import InCampaign
from .doi import HasDOI
Expand All @@ -28,8 +30,6 @@ class Site(SingleLocationData, Base, InCampaign, HasDOI):

name = Column(String()) # This can be pit_id
description = Column(String())
# Date of the measurement
date = Column(Date)

# Link the observer
# id is a mapped column for many-to-many with observers
Expand All @@ -53,3 +53,17 @@ class Site(SingleLocationData, Base, InCampaign, HasDOI):
vegetation_height = Column(String())
tree_canopy = Column(String())
site_notes = Column(String())

@hybrid_property
def date(self):
"""
Helper attribute to only query for dates of measurements
"""
return self.datetime.date()

@date.expression
def date(cls):
"""
Helper attribute to only query for dates of measurements
"""
return cls.datetime.cast(Date)
18 changes: 9 additions & 9 deletions tests/api/test_point_measurements.py
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ def setup_method(self, point_data):

def test_date_and_instrument(self):
result = self.subject.from_filter(
date=self.db_data.date.date(),
date=self.db_data.datetime.date(),
instrument=self.db_data.observation.instrument.name,
)
assert len(result) == 1
Expand All @@ -110,7 +110,7 @@ def test_instrument_and_limit(self, point_data_factory):

def test_no_instrument_on_date(self):
result = self.subject.from_filter(
date=self.db_data.date.date() + timedelta(days=1),
date=self.db_data.datetime.date() + timedelta(days=1),
instrument=self.db_data.observation.instrument.name,
)
assert len(result) == 0
Expand All @@ -123,7 +123,7 @@ def test_unknown_instrument(self):

def test_date_and_measurement_type(self):
result = self.subject.from_filter(
date=self.db_data.date.date(),
date=self.db_data.datetime.date(),
type=self.db_data.observation.measurement_type.name,
)
assert len(result) == 1
Expand All @@ -145,21 +145,21 @@ def test_observer_in_campaign(self):
assert result.loc[0].value == self.db_data.value

def test_date_less_equal(self, point_data_factory):
greater_date = self.db_data.date.date() + timedelta(days=1)
point_data_factory.create(date=greater_date)
greater_date = self.db_data.datetime.date() + timedelta(days=1)
point_data_factory.create(datetime=greater_date)

result = self.subject.from_filter(
date_less_equal=self.db_data.date.date(),
date_less_equal=self.db_data.datetime.date(),
)
assert len(result) == 1
assert result.loc[0].value == self.db_data.value

def test_date_greater_equal(self, point_data_factory):
greater_date = self.db_data.date.date() - timedelta(days=1)
point_data_factory.create(date=greater_date)
greater_date = self.db_data.datetime.date() - timedelta(days=1)
point_data_factory.create(datetime=greater_date)

result = self.subject.from_filter(
date_greater_equal=self.db_data.date.date(),
date_greater_equal=self.db_data.datetime.date(),
)
assert len(result) == 1
assert result.loc[0].value == self.db_data.value
Expand Down
8 changes: 3 additions & 5 deletions tests/db_connection.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from datetime import date, time
from datetime import datetime

import pytest
from geoalchemy2.elements import WKTElement
Expand Down Expand Up @@ -77,9 +77,8 @@ def _add_entry(
session, Site, dict(name=site_name),
object_kwargs=dict(
name=site_name, campaign=campaign,
date=kwargs.pop("date"),
datetime=kwargs.pop("datetime"),
geom=kwargs.pop("geom"),
time=kwargs.pop("time"),
elevation=kwargs.pop("elevation"),
observers=observer_list,
)
Expand Down Expand Up @@ -119,8 +118,7 @@ def _add_entry(
def populated_layer(self, db):
# Fake data to implement
row = {
'date': date(2020, 1, 28),
'time': time(18, 48),
'datetime': datetime(2020, 1, 28, 18, 48),
'elevation': 3148.2,
'geom': WKTElement(
"POINT(747987.6190615438 4324061.7062127385)", srid=26912
Expand Down
4 changes: 2 additions & 2 deletions tests/factories/point_data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import datetime
from datetime import datetime, timezone

import factory
from geoalchemy2 import WKTElement
Expand All @@ -13,7 +13,7 @@ class Meta:
model = PointData

value = 10
date = factory.LazyFunction(datetime.datetime.now)
datetime = factory.LazyFunction(lambda: datetime.now(timezone.utc))

geom = WKTElement(
"POINT(747987.6190615438 4324061.7062127385)", srid=26912
Expand Down
4 changes: 2 additions & 2 deletions tests/factories/site.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import datetime
from datetime import datetime, timezone

import factory
from geoalchemy2 import WKTElement
Expand All @@ -15,7 +15,7 @@ class Meta:

name = 'Site Name'
description = 'Site Description'
date = factory.LazyFunction(datetime.date.today)
datetime = factory.LazyFunction(lambda: datetime.now(timezone.utc))

slope_angle = 0.0
aspect = 0.0
Expand Down
28 changes: 21 additions & 7 deletions tests/tables/test_point_data.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,11 @@
from snowexsql.tables import PointData


@pytest.fixture
def point_data_attributes(site_factory):
return site_factory.build()


@pytest.fixture
def point_entry_record(point_data_factory, db_session):
point_data_factory.create()
Expand All @@ -14,23 +19,32 @@ def point_entry_record(point_data_factory, db_session):

class TestPointData:
@pytest.fixture(autouse=True)
def setup_method(self, point_entry_record):
def setup_method(self, point_entry_record, point_data_attributes):
self.subject = point_entry_record
self.attributes = point_data_attributes

def test_record_id(self):
assert self.subject.id is not None

def test_value_attribute(self):
assert type(self.subject.value) is float

def test_date_attribute(self):
assert type(self.subject.date) is datetime
def test_datetime_attribute(self):
assert type(self.subject.datetime) is datetime
# The microseconds won't be the same between the site_attribute
# and site_record fixture. Hence only testing the difference being
# small. Important to subtract the later from the earlier time as
# the timedelta object is incorrect otherwise
assert (
self.attributes.datetime - self.subject.datetime
).seconds == pytest.approx(0, rel=0.1)

def test_date_only_attribute(self):
assert type(self.subject.date_only) is date
def test_date_attribute(self):
assert type(self.subject.date) is date
assert self.subject.date == self.attributes.date

def test_elevation_attribute(self, point_data_factory):
assert self.subject.elevation == point_data_factory.elevation
def test_elevation_attribute(self):
assert self.subject.elevation == self.attributes.elevation

def test_geom_attribute(self):
assert isinstance(self.subject.geom, WKBElement)
10 changes: 10 additions & 0 deletions tests/tables/test_site.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ def setup_method(self, site_record, site_attributes):
def test_site_name(self, site_factory):
assert self.subject.name == site_factory.name

def test_datetime_attribute(self):
assert type(self.subject.datetime) is datetime.datetime
# The microseconds won't be the same between the site_attribute
# and site_record fixture. Hence only testing the difference being
# small. Important to subtract the later from the earlier time as
# the timedelta object is incorrect otherwise
assert (
self.attributes.datetime - self.subject.datetime
).seconds == pytest.approx(0, rel=0.1)

def test_date_attribute(self):
assert type(self.subject.date) is datetime.date
assert self.subject.date == self.attributes.date
Expand Down

0 comments on commit e87c564

Please sign in to comment.