"""
SnowEx API for querying measurement data.
This module has the query logic for the Python API and the LambdaClient.
See the lambda_handler.py for requirements on exposing an endpoint to the client.
"""
import logging
import os
import pandas as pd
from sqlalchemy import Numeric, cast, exists, literal
from sqlalchemy.dialects import postgresql
from sqlalchemy.sql import func
from snowexsql.db import db_session_with_credentials
from snowexsql.tables import (
DOI,
Campaign,
CampaignObservation,
ImageData,
ImageObservation,
Instrument,
LayerData,
MeasurementType,
Observer,
PointData,
PointObservation,
Site,
)
# Initialize logger first
LOG = logging.getLogger(__name__)
def query_to_geopandas(query, engine, **kwargs):
"""
Convert SQLAlchemy query to GeoDataFrame (if geopandas available) or DataFrame.
Execution context:
- Local API: Returns GeoDataFrame with proper geometry objects
- Lambda client: Returns pandas DataFrame
- DataFrame is serialized to JSON by lambda_handler
- lambda_client receives JSON and converts to GeoDataFrame client-side
Args:
query: SQLAlchemy Query object
engine: SQLAlchemy engine
**kwargs: Additional arguments passed to read_postgis or read_sql
Returns:
Local API: geopandas.GeoDataFrame
Lambda client: pandas.DataFrame
"""
sql = query.statement.compile(dialect=postgresql.dialect())
try:
import geopandas as gpd
return gpd.read_postgis(sql, engine.connect(), **kwargs)
except ImportError:
# Geopandas not available (e.g., Lambda environment)
# Returns pandas DataFrame with geometry as WKB/WKT
# lambda_client will convert to GeoDataFrame client-side
return pd.read_sql(sql, engine, **kwargs)
def raster_to_rasterio(rasters):
"""Raster functionality requires rasterio"""
raise ImportError(
"Raster functionality not available in Lambda environment. "
"Use local API for raster operations."
)
[docs]
class LargeQueryCheckException(RuntimeError):
pass
def get_points():
"""
Get a single row from the points table
"""
with db_session_with_credentials() as (_engine, session):
qry = session.query(PointData).limit(1)
# Execute that query!
result = qry.all()
# TODO:
# * Possible enums
# * implement 'like' or 'contains' method
[docs]
class BaseDataset:
"""
Base API access class for all SnowEx measurement dataset providing common
query methods and shared attributes.
Provides filtering, querying, and property-based access to measurements
stored in the SnowEx database. Subclasses must set ``MODEL`` to the
appropriate SQLAlchemy table class.
"""
MODEL = None
ALLOWED_QRY_KWARGS = [
"campaign",
"date",
"instrument",
"type",
"utm_zone",
"date_greater_equal",
"date_less_equal",
"value_greater_equal",
"value_less_equal",
"doi",
"observer",
]
SPECIAL_KWARGS = ["limit"]
# Default max record count
MAX_RECORD_COUNT = 1000
[docs]
@staticmethod
def retrieve_single_value_result(result):
"""
When we only request a single thing we still get a list of
lists this function filters it out. This usually looks like a
list of tuples.
"""
final = []
if len(result) != 0:
final = [r[0] for r in result]
return final
@classmethod
def _build_select_clause(cls, verbose=False):
"""
Build the SELECT clause for queries.
Override in subclasses to customize columns returned based on verbose
mode.
Args:
verbose: If True, return denormalized data with related table
columns
Returns:
List of SQLAlchemy column expressions for session.query()
"""
# Default: just return the model (all its direct columns)
return [cls.MODEL]
@classmethod
def _check_size(cls, qry, kwargs):
"""
Safeguard against accidental giant requests
"""
count = qry.count()
if count > cls.MAX_RECORD_COUNT and "limit" not in kwargs:
raise LargeQueryCheckException(
f"Query will return {count} number of records,"
f" but we have a default max of {cls.MAX_RECORD_COUNT}."
f" If you want to proceed, set the 'limit' filter"
f" to the desired number of records."
)
@classmethod
def _filter_campaign(cls, qry, v):
qry = qry.filter(Site.campaign.has(Campaign.name == v))
return qry
@classmethod
def _filter_observers(cls, qry, v):
qry = qry.join(cls.MODEL.observers).filter(Observer.name == v)
return qry
@classmethod
def _filter_instrument(cls, qry, value):
return qry.filter(cls.MODEL.instrument.has(name=value))
@classmethod
def _filter_measurement_type(cls, qry, value):
return qry.join(cls.MODEL.measurement_type).filter(
MeasurementType.name == value
)
@classmethod
def _filter_doi(cls, qry, value):
return qry.join(cls.MODEL.doi).filter(DOI.doi == value)
[docs]
@classmethod
def extend_qry(cls, qry, check_size=True, **kwargs):
if cls.MODEL is None:
raise ValueError("You must use a class with a MODEL.")
# use the default kwargs
for k, v in kwargs.items():
# Handle special operations
if k in cls.ALLOWED_QRY_KWARGS:
qry_model = cls.MODEL
# Logic for filtering on date with LayerData
if "date" in k and cls.MODEL == LayerData:
qry = qry.join(LayerData.site)
qry_model = Site
elif cls.MODEL == PointData:
qry = qry.join(PointData.observation)
# standard filtering using qry.filter
if isinstance(v, list):
filter_col = getattr(qry_model, k)
if k == "date":
raise ValueError("We cannot search for a list of dates")
elif "_equal" in k:
raise ValueError(
"We cannot compare greater_equal or less_equal with a list"
)
elif k == "site":
# Skip list handling here, will be handled below
pass
else:
qry = qry.filter(filter_col.in_(v))
LOG.debug(f"Filtering {k} to value {v}")
else:
# Filter boundary
if "_greater_equal" in k:
key = k.split("_greater_equal")[0]
if key == "value":
qry = qry.filter(
cast(getattr(qry_model, key), Numeric) >= v
)
else:
qry = qry.filter(getattr(qry_model, key) >= v)
elif "_less_equal" in k:
key = k.split("_less_equal")[0]
if key == "value":
qry = qry.filter(
cast(getattr(qry_model, key), Numeric) <= v
)
else:
qry = qry.filter(getattr(qry_model, key) <= v)
# Filter linked columns
elif k == "instrument":
qry = cls._filter_instrument(qry, v)
elif k == "campaign":
qry = cls._filter_campaign(qry, v)
elif k == "site":
# Handle list of site names
if isinstance(v, list):
qry = qry.filter(
exists()
.where(qry_model.site_id == Site.id)
.where(Site.name.in_(v))
)
else:
qry = qry.filter(qry_model.site.has(name=v))
elif k == "observer":
qry = cls._filter_observers(qry, v)
elif k == "doi":
qry = cls._filter_doi(qry, v)
elif k == "type":
qry = cls._filter_measurement_type(qry, v)
# Filter to exact value
else:
qry = qry.filter(getattr(qry_model, k) == v)
LOG.debug(f"Filtering {k} to list {v}")
# to avoid limit before filter
elif k in cls.SPECIAL_KWARGS:
if k == "limit":
qry = qry.limit(v)
else:
# Error out for not-allowed kwargs
raise ValueError(f"{k} is not an allowed filter")
if check_size:
cls._check_size(qry, kwargs)
return qry
[docs]
@classmethod
def from_unique_entries(cls, columns_to_search, **kwargs):
"""
Returns unique values from a column to help with filtering
"""
columns = [getattr(cls.MODEL, column) for column in columns_to_search]
with db_session_with_credentials() as (_engine, session):
try:
qry = session.query(*columns)
# Hardcode the limit to
qry = cls.extend_qry(qry, check_size=False, **kwargs)
results = qry.distinct().all()
except Exception as e:
session.close()
LOG.error("Failed query finding options for filtering")
raise e
if len(columns_to_search) == 1:
results = cls.retrieve_single_value_result(results)
return results
[docs]
@classmethod
def from_filter(cls, verbose=False, **kwargs):
"""
Get data for the class by filtering by allowed arguments. The allowed
filters are cls.ALLOWED_QRY_KWARGS.
Args:
verbose: If True, return denormalized data with related table columns
kwargs: Filter arguments from ALLOWED_QRY_KWARGS
"""
with db_session_with_credentials() as (engine, session):
try:
select_clause = cls._build_select_clause(verbose)
qry = session.query(*select_clause)
# Add explicit joins for verbose mode to avoid cartesian
# products
if verbose and hasattr(cls, "_add_verbose_joins"):
qry = cls._add_verbose_joins(qry)
elif hasattr(cls, "_add_base_joins"):
# For verbose=False, still need basic joins (e.g.,
# Site for geom)
qry = cls._add_base_joins(qry)
qry = cls.extend_qry(qry, **kwargs)
# For debugging in the test suite and not
# recommended in production
# https://docs.sqlalchemy.org/en/20/faq/
# sqlexpressions.html#rendering-postcompile-
# parameters-as-bound-parameters
if "DEBUG_QUERY" in os.environ:
full_sql_query = qry.statement.compile(
compile_kwargs={"literal_binds": True}
)
print("\n ** SQL query **")
print(full_sql_query)
df = query_to_geopandas(qry, engine)
except Exception as e:
session.close()
LOG.error("Failed query for PointData")
raise e
return df
[docs]
@classmethod
def from_area(
cls, verbose=False, shp=None, pt=None, buffer=None, crs=26912, **kwargs
):
"""
Get data for the class within a specific shapefile or
within a point and a known buffer. Uses PostGIS functions via ORM
for spatial operations, eliminating dependency on geoalchemy2/shapely.
Args:
verbose: If True, return denormalized data with related table columns
shp: shapely geometry in which to filter, or WKT string
pt: shapely point that will have a buffer applied, or WKT string
buffer: buffer distance in same units as point (meters if using geography)
crs: integer SRID/EPSG code (default 26912 = UTM Zone 12N)
kwargs: for more filtering or limiting (cls.ALLOWED_QRY_KWARGS)
Returns:
pandas DataFrame with results (includes geom column with WKT)
"""
if shp is None and pt is None:
raise ValueError("Inputs must be a shape description or a point and buffer")
if (pt is not None and buffer is None) or (buffer is not None and pt is None):
raise ValueError("pt and buffer must be given together")
# Convert shapely objects to WKT if needed
if shp is not None and hasattr(shp, "wkt"):
shp_wkt = shp.wkt
elif isinstance(shp, str):
shp_wkt = shp
else:
shp_wkt = None
if pt is not None:
if hasattr(pt, "wkt"):
pt_wkt = pt.wkt
elif isinstance(pt, str):
pt_wkt = pt
elif isinstance(pt, (tuple, list)) and len(pt) == 2:
# Handle (x, y) tuple format
pt_wkt = f"POINT ({pt[0]} {pt[1]})"
else:
pt_wkt = None
else:
pt_wkt = None
# Determine table structure
table_name = cls.MODEL.__tablename__
needs_site_join = table_name == "layers"
with db_session_with_credentials() as (engine, session):
try:
# Detect database SRID to avoid transforming indexed column
# Query first non-null geometry to determine database SRID
if needs_site_join:
srid_qry = (
session.query(func.ST_SRID(Site.geom))
.filter(Site.geom.isnot(None))
.limit(1)
)
else:
srid_qry = (
session.query(func.ST_SRID(cls.MODEL.geom))
.filter(cls.MODEL.geom.isnot(None))
.limit(1)
)
try:
db_srid_result = session.execute(srid_qry).first()
if not db_srid_result or db_srid_result[0] is None:
# No data in table yet - use input CRS as default
# This allows empty table queries to work
# (will return empty)
LOG.warning(
f"No geometries found in {table_name}, "
f"using input CRS {crs} as default"
)
db_srid = crs
else:
db_srid = db_srid_result[0]
LOG.debug(
f"Detected database SRID: \
{db_srid} for table {table_name}"
)
except Exception as srid_error:
# If SRID detection fails, fall back to input CRS
LOG.warning(
f"SRID detection failed for {table_name}: {srid_error}."
f"Using input CRS {crs} as default"
)
db_srid = crs
# Build PostGIS search geometry
# Transform search geometry to match database SRID for index
# usage
if pt_wkt:
# Create point in input CRS, buffer it, then transform to
# DB SRID
# Buffer before transform to ensure correct distance units
search_geom = func.ST_Transform(
func.ST_Buffer(
func.ST_GeomFromText(literal(pt_wkt), literal(crs)),
literal(buffer),
),
literal(db_srid),
)
elif shp_wkt:
# Transform shape from input CRS to database SRID
search_geom = func.ST_Transform(
func.ST_GeomFromText(literal(shp_wkt), literal(crs)),
literal(db_srid),
)
else:
raise ValueError("Unable to parse geometry input")
# Build select clause (handles verbose mode)
select_clause = (
cls._build_select_clause(verbose)
if hasattr(cls, "_build_select_clause")
else [cls.MODEL]
)
qry = session.query(*select_clause)
# Add explicit joins for verbose mode to avoid
# cartesian products
if verbose and hasattr(cls, "_add_verbose_joins"):
qry = cls._add_verbose_joins(qry)
elif hasattr(cls, "_add_base_joins"):
# For verbose=False, still need basic joins
# (e.g., Site for geom)
qry = cls._add_base_joins(qry)
# Add spatial filter
if needs_site_join:
# For LayerData, join to Site for geometry
# SQLAlchemy handles duplicate joins from _add_*_joins above
qry = qry.join(cls.MODEL.site)
qry = qry.filter(func.ST_Intersects(Site.geom, search_geom))
else:
# For PointData, use direct geometry column
qry = qry.filter(func.ST_Intersects(cls.MODEL.geom, search_geom))
# Add standard filters using existing extend_qry
# This handles type, instrument, campaign, date ranges, etc.
qry = cls.extend_qry(qry, **kwargs)
# Execute and convert to GeoDataFrame
df = query_to_geopandas(qry, engine)
except Exception as e:
session.close()
LOG.error(f"Failed query for {cls.__name__}")
raise e
return df
@property
def all_campaigns(self):
"""
Return all campaign names
"""
with db_session_with_credentials() as (_engine, session):
qry = session.query(Campaign.name).distinct()
result = qry.all()
return self.retrieve_single_value_result(result)
@property
def all_types(self):
"""
Return all types of the data
"""
with db_session_with_credentials() as (_engine, session):
qry = session.query(MeasurementType.name).distinct()
result = qry.all()
return self.retrieve_single_value_result(result)
@property
def all_dates(self):
"""
Return all distinct dates in the data
"""
with db_session_with_credentials() as (_engine, session):
qry = session.query(self.MODEL.date).distinct()
result = qry.all()
return self.retrieve_single_value_result(result)
@property
def all_observers(self):
"""
Return all distinct observers in the data
"""
with db_session_with_credentials() as (_engine, session):
qry = session.query(Observer.name).distinct()
result = qry.all()
return self.retrieve_single_value_result(result)
@property
def all_dois(self):
"""
Return all distinct DOIs in the data
"""
with db_session_with_credentials() as (_engine, session):
qry = session.query(DOI.doi).distinct()
result = qry.all()
return self.retrieve_single_value_result(result)
@property
def all_units(self):
"""
Return all distinct units in the data
"""
with db_session_with_credentials() as (_engine, session):
qry = session.query(self.MODEL.units).distinct()
result = qry.all()
return self.retrieve_single_value_result(result)
@property
def all_instruments(self):
"""
Return all distinct instruments in the data
"""
with db_session_with_credentials() as (_engine, session):
# Use EXISTS for better performance on large datasets
# (29GB+ tables)
qry = session.query(Instrument.name).filter(
exists().where(self.MODEL.instrument_id == Instrument.id)
)
result = qry.all()
return self.retrieve_single_value_result(result)
[docs]
class PointMeasurements(BaseDataset):
"""
API class for access to PointData from a single location.
Examples: Snow Depth, SWE
"""
MODEL = PointData
@classmethod
def _build_select_clause(cls, verbose=False):
"""
Build SELECT clause for PointMeasurements queries.
Args:
verbose: If False, return only core point columns.
If True, return denormalized data with observation,
instrument, and measurement type info.
"""
if verbose:
# Return denormalized data with meaningful column names
# Note: Must also join these tables explicitly in the query
return [
cls.MODEL.value,
cls.MODEL.datetime.label("date"),
cls.MODEL.elevation,
cls.MODEL.geom,
CampaignObservation.name.label("observation_name"),
CampaignObservation.description.label("obs_description"),
MeasurementType.name.label("type"),
MeasurementType.units.label("units"),
MeasurementType.derived.label("derived"),
Instrument.name.label("instrument_name"),
Instrument.model.label("instrument_model"),
Instrument.specifications.label("instrument_specifications"),
Campaign.name.label("campaign_name"),
Observer.name.label("observer_name"),
]
else:
# Return only columns from points table
return [
cls.MODEL.id,
cls.MODEL.value,
cls.MODEL.datetime,
cls.MODEL.elevation,
cls.MODEL.geom,
]
@classmethod
def _add_verbose_joins(cls, qry):
"""
Add explicit joins needed for verbose mode to avoid cartesian products.
"""
qry = qry.join(cls.MODEL.observation)
qry = qry.join(cls.MODEL.measurement_type)
qry = qry.join(PointObservation.instrument)
qry = qry.join(PointObservation.campaign)
qry = qry.join(PointObservation.observer)
return qry
@classmethod
def _filter_campaign(cls, qry, value):
return (
qry.join(cls.MODEL.observation)
.join(PointObservation.campaign)
.filter(Campaign.name == value)
)
@classmethod
def _filter_instrument(cls, qry, value):
return (
qry.join(cls.MODEL.observation)
.join(PointObservation.instrument)
.filter(Instrument.name == value)
)
@classmethod
def _filter_doi(cls, qry, value):
return (
qry.join(cls.MODEL.observation)
.join(PointObservation.doi)
.filter(DOI.doi == value)
)
@classmethod
def _filter_observers(cls, qry, value):
return (
qry.join(cls.MODEL.observation)
.join(PointObservation.observer)
.filter(Observer.name == value)
)
@property
def all_types(self):
"""
Return all measurement types that have data in the points table
"""
with db_session_with_credentials() as (_engine, session):
# Use EXISTS for better performance on large points table
qry = session.query(MeasurementType.name).filter(
exists().where(PointData.measurement_type_id == MeasurementType.id)
)
result = qry.all()
return self.retrieve_single_value_result(result)
@property
def all_instruments(self):
"""
Return all distinct instruments in the data
"""
with db_session_with_credentials() as (_engine, session):
result = (
session.query(Instrument.name)
.filter(
Instrument.id.in_(
session.query(PointObservation.instrument_id).distinct()
)
)
.distinct()
.all()
)
return self.retrieve_single_value_result(result)
class TooManyRastersException(Exception):
"""
Exception to report to users that their query will produce too
many rasters
"""
[docs]
class LayerMeasurements(BaseDataset):
"""
API class for access to LayerData.
Examples: Snow pit observations (depth, density, temperature), SMP
"""
MODEL = LayerData
ALLOWED_QRY_KWARGS = [
"campaign",
"site",
"date",
"instrument",
"observer",
"type",
"utm_zone",
"date_greater_equal",
"date_less_equal",
"doi",
"value_greater_equal",
"value_less_equal",
]
@classmethod
def _filter_campaign(cls, qry, v):
return qry.join(cls.MODEL.site).join(Site.campaign).filter(Campaign.name == v)
@classmethod
def _filter_observers(cls, qry, v):
return qry.join(cls.MODEL.site).join(Site.observers).filter(Observer.name == v)
@classmethod
def _filter_doi(cls, qry, value):
return qry.join(cls.MODEL.site).join(Site.doi).filter(DOI.doi == value)
@classmethod
def _build_select_clause(cls, verbose=False):
"""
Build SELECT clause for LayerMeasurements queries.
Args:
verbose: If False, return only core layer columns (no joins).
If True, return denormalized data with site, instrument,
and measurement type info.
"""
if verbose:
# Return denormalized data with meaningful column names
# Note: Must also join these tables explicitly in the query
return [
cls.MODEL.depth,
cls.MODEL.bottom_depth,
cls.MODEL.value,
Site.name.label("site_name"),
Site.description.label("site_description"),
Site.slope_angle.label("slope_angle"),
Site.aspect.label("aspect"),
Site.air_temp.label("air_temp"),
Site.total_depth.label("total_depth"),
Site.weather_description.label("weather_description"),
Site.precip.label("precip"),
Site.sky_cover.label("sky_cover"),
Site.wind.label("wind"),
Site.ground_condition.label("ground_condition"),
Site.ground_roughness.label("ground_roughness"),
Site.ground_vegetation.label("ground_vegetation"),
Site.vegetation_height.label("vegetation_height"),
Site.tree_canopy.label("tree_canopy"),
Site.comments.label("site_comments"),
Site.datetime.label("date"),
Site.geom,
MeasurementType.name.label("type"),
MeasurementType.units.label("units"),
MeasurementType.derived.label("type_derived"),
Instrument.name.label("instrument_name"),
Instrument.model.label("instrument_model"),
Instrument.specifications.label("instrument_specifications"),
func.ST_AsText(Site.geom).label("geom_wkt"),
]
else:
# Return only core columns from layers table plus geom for geopandas
return [
cls.MODEL.id,
cls.MODEL.depth,
cls.MODEL.bottom_depth,
cls.MODEL.value,
Site.geom, # Required for GeoDataFrame
]
@classmethod
def _add_base_joins(cls, qry):
"""
Add minimal joins needed for non-verbose mode (just Site for geom).
"""
qry = qry.join(cls.MODEL.site)
return qry
@classmethod
def _add_verbose_joins(cls, qry):
"""
Add explicit joins needed for verbose mode to avoid cartesian products.
"""
qry = qry.join(cls.MODEL.site)
qry = qry.join(cls.MODEL.measurement_type)
qry = qry.join(cls.MODEL.instrument)
return qry
@property
def all_types(self):
"""
Return all measurement types that have data in the layers table
"""
with db_session_with_credentials() as (_engine, session):
# Use EXISTS for better performance on 208M row table
qry = session.query(MeasurementType.name).filter(
exists().where(LayerData.measurement_type_id == MeasurementType.id)
)
result = qry.all()
return self.retrieve_single_value_result(result)
@property
def all_sites(self):
"""
Return all specific site names
"""
with db_session_with_credentials() as (_engine, session):
result = session.query(Site.name).distinct().all()
return self.retrieve_single_value_result(result)
@property
def all_dates(self):
"""
Return all distinct dates in the data
"""
with db_session_with_credentials() as (_engine, session):
result = session.query(Site.date).distinct().all()
return self.retrieve_single_value_result(result)
@property
def all_units(self):
"""
Return all distinct units in the data
"""
with db_session_with_credentials() as (_engine, session):
result = session.query(MeasurementType.units).distinct().all()
return self.retrieve_single_value_result(result)
[docs]
@classmethod
def get_sites(cls, site_names=None, **kwargs):
"""
Get site information including geometries.
Args:
site_names: List of site names or single site name
kwargs: Additional filters (campaign, date, etc.)
Returns:
GeoDataFrame with site information
"""
with db_session_with_credentials() as (engine, session):
qry = session.query(
Site.name, Site.geom, Site.description, Site.datetime
).distinct()
# others can be added
if site_names is not None:
if isinstance(site_names, list):
qry = qry.filter(Site.name.in_(site_names))
else:
qry = qry.filter(Site.name == site_names)
df = query_to_geopandas(qry, engine)
return df
class RasterMeasurements(BaseDataset):
"""
Early beta class for handling data stored in raster datasets.
"""
MODEL = ImageData
ALLOWED_QRY_KWARGS = BaseDataset.ALLOWED_QRY_KWARGS + ["description"]
@property
def all_types(self):
"""
Return all measurement types that have data in the images table
"""
with db_session_with_credentials() as (_engine, session):
result = (
session.query(MeasurementType.name)
.join(ImageData, ImageData.observation_id == ImageObservation.id)
.join(ImageObservation.measurement_type)
.distinct()
.all()
)
return self.retrieve_single_value_result(result)
@property
def all_descriptions(self):
with db_session_with_credentials() as (_engine, session):
qry = session.query(self.MODEL.description).distinct()
result = qry.all()
return self.retrieve_single_value_result(result)
@classmethod
def check_for_single_dataset(cls, **kwargs):
"""
At the moment there is not a clear path to how to deal with
multiple rasters so check that the user only requested one
dataset
"""
LOG.info("Checking raster query for single raster dataset...")
multi_raster_indicators = [
"instrument",
"date",
"observers",
"doi",
"type",
"description",
]
with db_session_with_credentials() as (_engine, session):
try:
# Form query and check if the query spans
# multiple rasters
for column in multi_raster_indicators:
values = cls.from_unique_entries([column], **kwargs)
if len(values) > 1:
options = [f"'{v}'" for v in values]
raise TooManyRastersException(
f"More than one `{column}` suggests "
f"there are multiple raster datasets. "
f"Try filter {column} to one of the "
f"following values {', '.join(options)}."
)
except Exception as e:
session.close()
LOG.error("Failed query for Raster Data")
raise e
@classmethod
def from_filter(cls, **kwargs):
"""
Get data for the class by filtering by allowed arguments.
The allowed filters are cls.ALLOWED_QRY_KWARGS.
"""
cls.check_for_single_dataset(**kwargs)
with db_session_with_credentials() as (_engine, session):
try:
# Rebuild the query and form the raster
base_query = cls.MODEL.raster
qry = session.query(
func.ST_AsTiff(func.ST_Union(base_query, type_=Raster))
)
qry = cls.extend_qry(qry, **kwargs)
rasters = qry.all()
# Get the rasterio object of the raster
datasets = raster_to_rasterio(rasters)
except Exception as e:
LOG.error("Failed query for Raster Data")
raise e
return datasets
@classmethod
def from_area(cls, shp=None, pt=None, buffer=None, crs=26912, **kwargs):
if shp is None and pt is None:
raise ValueError("We need a shape description or a point and buffer")
if (pt is not None and buffer is None) or (buffer is not None and pt is None):
raise ValueError("pt and buffer must be given together")
# Check if geometric operations are available
if not HAS_CORE_GIS:
raise ImportError(
"geoalchemy2 and shapely are required for "
"geometric filtering. Install with: "
"pip install geoalchemy2 shapely"
)
with db_session_with_credentials() as (_engine, session):
try:
# Get shape ready for cropping with rasters
if shp:
db_shp = from_shape(shp, srid=crs)
else:
qry_pt = from_shape(pt)
qry = session.query(
gfunc.ST_SetSRID(func.ST_Buffer(qry_pt, buffer), crs)
)
db_shp = qry.all()[0][0]
# Grab the rasters, union and clip them
base_query = func.ST_AsTiff(
func.ST_Clip(
func.ST_Union(ImageData.raster, type_=Raster), db_shp, True
)
)
q = session.query(base_query)
# Find all the tiles that
q = q.filter(gfunc.ST_Intersects(ImageData.raster, db_shp))
limit = kwargs.get("limit")
if limit:
kwargs.pop("limit")
q = cls.extend_qry(q, check_size=False, **kwargs)
rasters = q.all()
# Get the rasterio object of the raster
datasets = raster_to_rasterio(rasters)
if len(datasets) > 0:
dataset = datasets[0]
else:
dataset = datasets
return dataset
except Exception as e:
LOG.error("Failed query for Raster Data")
raise e