From 7db0cce5984218338fe074d0073003b0953fce03 Mon Sep 17 00:00:00 2001 From: Vin Date: Mon, 16 Mar 2026 06:15:45 -0400 Subject: [PATCH] Major fastpheno refactor: Re-designed ERD to accompany more data (columns) especially the confidence column, made more endpoints that will be useful for the frontend such as timeseries comparisons --- api/models/fastpheno.py | 105 ++++++---- api/resources/fastpheno.py | 305 ++++++++++++++++++++++------ config/databases/fastpheno.sql | 240 ++++++++++++++++------ tests/resources/test_fastpheno.py | 325 ++++++++++++++++++++++-------- 4 files changed, 729 insertions(+), 246 deletions(-) diff --git a/api/models/fastpheno.py b/api/models/fastpheno.py index abaa1a3a..1804172f 100644 --- a/api/models/fastpheno.py +++ b/api/models/fastpheno.py @@ -1,65 +1,84 @@ +from datetime import datetime from api import db -import enum -from sqlalchemy.dialects.mysql import DECIMAL, ENUM -from sqlalchemy.orm import relationship -from sqlalchemy import ForeignKey -from typing import List +from sqlalchemy.dialects.mysql import DECIMAL class Sites(db.Model): __bind_key__ = "fastpheno" __tablename__ = "sites" - sites_pk: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False, primary_key=True) + sites_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) site_name: db.Mapped[str] = db.mapped_column(db.String(45), nullable=False) - site_desc: db.Mapped[str] = db.mapped_column(db.String(99), nullable=True) - children: db.Mapped[List["Trees"]] = relationship() + lat: db.Mapped[float] = db.mapped_column(DECIMAL(15, 12), nullable=False) + lng: db.Mapped[float] = db.mapped_column(DECIMAL(15, 12), nullable=False) + site_desc: db.Mapped[str] = db.mapped_column(db.String(999), nullable=True) + + +class Flights(db.Model): + __bind_key__ = "fastpheno" + __tablename__ = "flights" + + flights_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + pilot: db.Mapped[str] = db.mapped_column(db.String(45), nullable=True) + flight_date: db.Mapped[datetime] = db.mapped_column(db.DateTime, nullable=False) + sites_pk: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False) + height: db.Mapped[float] = db.mapped_column(DECIMAL(15, 10), nullable=True) + speed: db.Mapped[float] = db.mapped_column(DECIMAL(15, 10), nullable=True) class Trees(db.Model): __bind_key__ = "fastpheno" __tablename__ = "trees" - trees_pk: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False, primary_key=True) - sites_pk: db.Mapped[int] = db.mapped_column(ForeignKey("sites.sites_pk")) - longitude: db.Mapped[float] = db.mapped_column(db.Float, nullable=False) - latitude: db.Mapped[float] = db.mapped_column(db.Float, nullable=False) - genotype_id: db.Mapped[str] = db.mapped_column(db.String(5), nullable=True) + trees_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + sites_pk: db.Mapped[int] = db.mapped_column(db.Integer, nullable=False) + longitude: db.Mapped[float] = db.mapped_column(DECIMAL(15, 12), nullable=False) + latitude: db.Mapped[float] = db.mapped_column(DECIMAL(15, 12), nullable=False) + tree_site_id: db.Mapped[str] = db.mapped_column(db.String(45), nullable=True) + family_id: db.Mapped[str] = db.mapped_column(db.String(45), nullable=True) external_link: db.Mapped[str] = db.mapped_column(db.String(200), nullable=True) - tree_given_id: db.Mapped[str] = db.mapped_column(db.String(25), nullable=True) - children: db.Mapped[List["Band"]] = relationship() - - -class MonthChoices(enum.Enum): - jan = "1" - feb = "2" - mar = "3" - apr = "4" - may = "5" - jun = "6" - jul = "7" - aug = "8" - sep = "9" - oct = "10" - nov = "11" - dec = "12" - - -class Band(db.Model): + block_num: db.Mapped[int] = db.mapped_column(db.Integer, nullable=True) + seq_id: db.Mapped[str] = db.mapped_column(db.String(25), nullable=True) + x_pos: db.Mapped[int] = db.mapped_column(db.Integer, nullable=True) + y_pos: db.Mapped[int] = db.mapped_column(db.Integer, nullable=True) + height_2022: db.Mapped[str] = db.mapped_column(db.String(10), nullable=True) + + +class TreesFlightsJoinTbl(db.Model): __bind_key__ = "fastpheno" - __tablename__ = "band" + __tablename__ = "trees_flights_join_tbl" + + trees_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + flights_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + confidence: db.Mapped[float] = db.mapped_column(DECIMAL(8, 5), nullable=True) + - trees_pk: db.Mapped[int] = db.mapped_column(ForeignKey("trees.trees_pk"), primary_key=True) - month: db.Mapped[str] = db.mapped_column(ENUM(MonthChoices), nullable=False, primary_key=True) - band: db.Mapped[float] = db.mapped_column(db.String(100), nullable=False, primary_key=True) +class Bands(db.Model): + __bind_key__ = "fastpheno" + __tablename__ = "bands" + __table_args__ = (db.Index("bands_flight_band_tree_idx", "flights_pk", "band", "trees_pk"),) + + trees_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + flights_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + band: db.Mapped[str] = db.mapped_column(db.String(20), primary_key=True, nullable=False) + value: db.Mapped[float] = db.mapped_column(DECIMAL(8, 5), nullable=False) + + +class Pigments(db.Model): + __bind_key__ = "fastpheno" + __tablename__ = "pigments" + + trees_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + flights_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + pigment: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) value: db.Mapped[float] = db.mapped_column(DECIMAL(20, 15), nullable=False) -class Height(db.Model): +class Unispec(db.Model): __bind_key__ = "fastpheno" - __tablename__ = "height" + __tablename__ = "unispec" - trees_pk: db.Mapped[int] = db.mapped_column(ForeignKey("trees.trees_pk"), primary_key=True) - month: db.Mapped[str] = db.mapped_column(ENUM(MonthChoices), nullable=False, primary_key=True) - tree_height_proxy: db.Mapped[float] = db.mapped_column(DECIMAL(20, 15), nullable=False) - ground_height_proxy: db.Mapped[float] = db.mapped_column(DECIMAL(20, 15), nullable=False) + trees_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + flights_pk: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + pigment: db.Mapped[int] = db.mapped_column(db.Integer, primary_key=True, nullable=False) + value: db.Mapped[float] = db.mapped_column(DECIMAL(20, 15), nullable=False) diff --git a/api/resources/fastpheno.py b/api/resources/fastpheno.py index ca576b49..8363f11c 100644 --- a/api/resources/fastpheno.py +++ b/api/resources/fastpheno.py @@ -6,9 +6,10 @@ import re +from flask import request from flask_restx import Namespace, Resource from api import db -from api.models.fastpheno import Sites, Trees, Band, Height +from api.models.fastpheno import Sites, Flights, Trees, TreesFlightsJoinTbl, Bands from api.utils.bar_utils import BARUtils from markupsafe import escape @@ -16,95 +17,281 @@ fastpheno = Namespace("FastPheno", description="FastPheno API service", path="/fastpheno") -@fastpheno.route("/get_bands///") +@fastpheno.route("/get_bands///") class FastPheno(Resource): - @fastpheno.param("site", _in="path", default="pintendre") - @fastpheno.param("month", _in="path", default="jan") - @fastpheno.param("band", _in="path", default="band_1") - def get(self, site, month, band): - """This end point returns band values for a specific site AND month-date""" - # Escape input data - site = escape(site).capitalize() - month = escape(month) - band = escape(band) - - # Validate input - if not re.search(r"^[a-z]{1,15}$", site, re.I): + @fastpheno.param("site", _in="path", default="Pintendre") + @fastpheno.param("flight_id", _in="path", default="14") + @fastpheno.param("band", _in="path", default="398nm") + def get(self, site, flight_id, band): + """Returns all band values for a given site, flight ID, and band name""" + site = str(escape(site)).capitalize() + flight_id = str(escape(flight_id)) + band = str(escape(band)) + + if not re.search(r"^[a-zA-Z]{1,15}$", site): return BARUtils.error_exit("Invalid site name"), 400 - if not re.search(r"^[a-z]{1,4}$", month, re.I): - return BARUtils.error_exit("Invalid month"), 400 + if not BARUtils.is_integer(flight_id): + return BARUtils.error_exit("Invalid flight ID"), 400 - if not re.search(r"^band_\d{1,8}$", band, re.I): + if not re.search(r"^[a-zA-Z0-9_]{1,20}$", band): return BARUtils.error_exit("Invalid band"), 400 rows = db.session.execute( - db.select(Sites, Trees, Height, Band) - .select_from(Sites) - .join(Trees, Trees.sites_pk == Sites.sites_pk) # don't need to use 2nd arg, for clarity... I set ORM rel - .join(Height, Height.trees_pk == Trees.trees_pk) - .join(Band, Band.trees_pk == Trees.trees_pk) - .where(Sites.site_name == site, Band.month == month, Height.month == month, Band.band == band) + db.select(Trees, TreesFlightsJoinTbl, Bands) + .select_from(Bands) + .join( + TreesFlightsJoinTbl, + (Bands.trees_pk == TreesFlightsJoinTbl.trees_pk) & (Bands.flights_pk == TreesFlightsJoinTbl.flights_pk), + ) + .join(Trees, Bands.trees_pk == Trees.trees_pk) + .join(Flights, Bands.flights_pk == Flights.flights_pk) + .join(Sites, Flights.sites_pk == Sites.sites_pk) + .where(Sites.site_name == site, Bands.flights_pk == int(flight_id), Bands.band == band) ).all() + + if len(rows) == 0: + return BARUtils.error_exit("No data found for the given parameters"), 400 + res = [ { - "site_name": s.site_name, - "tree_id": t.trees_pk, - "longitude": t.longitude, - "latitutde": t.latitude, - "genotype_id": t.genotype_id, - "tree_given_id": t.tree_given_id, - "external_link": t.external_link, + "trees_pk": t.trees_pk, + "seq_id": t.seq_id, + "longitude": float(t.longitude), + "latitude": float(t.latitude), + "x_pos": t.x_pos, + "y_pos": t.y_pos, + "height_2022": t.height_2022, + "block_num": t.block_num, + "tree_site_id": t.tree_site_id, + "confidence": float(tf.confidence) if tf.confidence is not None else None, "band_value": float(b.value), - "tree_height_proxy": float(h.tree_height_proxy), - "ground_height_proxy": float(h.ground_height_proxy), - "band_month": b.month.name, } - for s, t, h, b in rows + for t, tf, b in rows ] - if len(rows) == 0: - return ( - BARUtils.error_exit("There are no data found for the given parameters"), - 400, - ) return BARUtils.success_exit(res) -@fastpheno.route("/get_trees/") +@fastpheno.route("/get_trees/") class FastPhenoTrees(Resource): - @fastpheno.param("genotype_id", _in="path", default="C") - def get(self, genotype_id): - """This end point returns trees for a given genotype_id across sites""" - # Escape input data - genotype_id = escape(genotype_id).capitalize() + @fastpheno.param("tree_site_id", _in="path", default="619") + @fastpheno.param("site", _in="query", required=False, default="Pintendre") + def get(self, tree_site_id): + """Returns trees for a given genotype. Accepts letter codes (e.g. 'c') or numeric + genotype prefixes (e.g. '619' matches '619.03'). Optionally filter by site name.""" + tree_site_id = str(escape(tree_site_id)) + site = request.args.get("site") - # Validate input - if not re.search(r"^[a-z]{1,3}$", genotype_id, re.I): - return BARUtils.error_exit("Invalid genotype id"), 400 + if not re.search(r"^[a-zA-Z0-9]{1,10}$", tree_site_id): + return BARUtils.error_exit("Invalid tree site ID"), 400 - rows = db.session.execute( + if site is not None: + site = str(escape(site)).capitalize() + if not re.search(r"^[a-zA-Z]{1,15}$", site): + return BARUtils.error_exit("Invalid site name"), 400 + + query = ( db.select(Sites, Trees) .select_from(Sites) .join(Trees, Trees.sites_pk == Sites.sites_pk) - .where(Trees.genotype_id == genotype_id) - ).all() + .where(db.or_(Trees.tree_site_id == tree_site_id, Trees.tree_site_id.like(f"{tree_site_id}.%"))) + ) + + if site is not None: + query = query.where(Sites.site_name == site) + + rows = db.session.execute(query).all() + + if len(rows) == 0: + return BARUtils.error_exit("No data found for the given parameters"), 400 + res = [ { "site_name": s.site_name, - "tree_id": t.trees_pk, - "longitude": t.longitude, - "latitutde": t.latitude, - "genotype_id": t.genotype_id, - "tree_given_id": t.tree_given_id, + "trees_pk": t.trees_pk, + "seq_id": t.seq_id, + "longitude": float(t.longitude), + "latitude": float(t.latitude), + "tree_site_id": t.tree_site_id, "external_link": t.external_link, } for s, t in rows ] + + return BARUtils.success_exit(res) + + +@fastpheno.route("/timeseries/tree//") +class FastPhenoTimeSeries(Resource): + @fastpheno.param("seq_id", _in="path", default="PIN_2547") + @fastpheno.param("band", _in="path", default="398nm") + def get(self, seq_id, band): + """Returns all band values + confidence for a single tree across all flights, ordered by flight date, for time series.""" + seq_id = str(escape(seq_id)).upper() + band = str(escape(band)) + + if not re.search(r"^[A-Z]{2,5}_\d{1,6}$", seq_id): + return BARUtils.error_exit("Invalid seq_id"), 400 + + if not re.search(r"^[a-zA-Z0-9_]{1,20}$", band): + return BARUtils.error_exit("Invalid band"), 400 + + rows = db.session.execute( + db.select(Flights, TreesFlightsJoinTbl, Bands) + .select_from(Bands) + .join( + TreesFlightsJoinTbl, + (Bands.trees_pk == TreesFlightsJoinTbl.trees_pk) & (Bands.flights_pk == TreesFlightsJoinTbl.flights_pk), + ) + .join(Trees, Bands.trees_pk == Trees.trees_pk) + .join(Flights, Bands.flights_pk == Flights.flights_pk) + .where(Trees.seq_id == seq_id, Bands.band == band) + .order_by(Flights.flight_date) + ).all() + if len(rows) == 0: - return ( - BARUtils.error_exit("There are no data found for the given parameters"), - 400, + return BARUtils.error_exit("No data found for the given parameters"), 400 + + res = [ + { + "flight_date": f.flight_date.isoformat(), + "flights_pk": f.flights_pk, + "confidence": float(tf.confidence) if tf.confidence is not None else None, + "band_value": float(b.value), + } + for f, tf, b in rows + ] + + return BARUtils.success_exit(res) + + +@fastpheno.route("/timeseries/genotype///aggregate") +class FastPhenoGenotypeTimeSeries(Resource): + @fastpheno.param("tree_site_id", _in="path", default="619") + @fastpheno.param("band", _in="path", default="398nm") + def get(self, tree_site_id, band): + """Returns AVG and STDDEV of band values per flight date across all trees sharing a + genotype (e.g. '619' matches all tree_site_id values starting with '619.'). Intended + for genotype-level time series plotting.""" + tree_site_id = str(escape(tree_site_id)) + band = str(escape(band)) + + if not re.search(r"^[a-zA-Z0-9]{1,10}$", tree_site_id): + return BARUtils.error_exit("Invalid tree site ID"), 400 + + if not re.search(r"^[a-zA-Z0-9_]{1,20}$", band): + return BARUtils.error_exit("Invalid band"), 400 + + rows = db.session.execute( + db.select( + Flights.flight_date, + Flights.flights_pk, + db.func.avg(Bands.value).label("avg_value"), + db.func.std(Bands.value).label("std_value"), + db.func.count(Bands.value).label("n_trees"), + ) + .select_from(Bands) + .join( + TreesFlightsJoinTbl, + (Bands.trees_pk == TreesFlightsJoinTbl.trees_pk) & (Bands.flights_pk == TreesFlightsJoinTbl.flights_pk), ) + .join(Trees, Bands.trees_pk == Trees.trees_pk) + .join(Flights, Bands.flights_pk == Flights.flights_pk) + .where( + db.or_(Trees.tree_site_id == tree_site_id, Trees.tree_site_id.like(f"{tree_site_id}.%")), + Bands.band == band, + ) + .group_by(Flights.flights_pk, Flights.flight_date) + .order_by(Flights.flight_date) + ).all() + + if len(rows) == 0: + return BARUtils.error_exit("No data found for the given parameters"), 400 + + res = [ + { + "flight_date": r.flight_date.isoformat(), + "flights_pk": r.flights_pk, + "avg_value": float(r.avg_value), + "std_value": float(r.std_value) if r.std_value is not None else None, + "n_trees": r.n_trees, + } + for r in rows + ] return BARUtils.success_exit(res) + + +@fastpheno.route("/sites") +class FastPhenoSites(Resource): + def get(self): + """Returns all sites with coordinates, for initializing the map view.""" + rows = db.session.execute( + db.select(Sites).order_by(Sites.site_name) + ).scalars().all() + + res = [ + { + "sites_pk": s.sites_pk, + "site_name": s.site_name, + "lat": float(s.lat), + "lng": float(s.lng), + "site_desc": s.site_desc, + } + for s in rows + ] + + return BARUtils.success_exit(res) + + +@fastpheno.route("/flights/") +class FastPhenoFlights(Resource): + @fastpheno.param("sites_pk", _in="path", default=1) + def get(self, sites_pk): + """Returns all flights for a given site, ordered by date, for populating the flight dropdown.""" + if not BARUtils.is_integer(str(sites_pk)): + return BARUtils.error_exit("Invalid sites_pk"), 400 + + rows = db.session.execute( + db.select(Flights) + .where(Flights.sites_pk == sites_pk) + .order_by(Flights.flight_date) + ).scalars().all() + + if len(rows) == 0: + return BARUtils.error_exit("No flights found for the given site"), 400 + + res = [ + { + "flights_pk": f.flights_pk, + "flight_date": f.flight_date.isoformat(), + "pilot": f.pilot, + "height": float(f.height) if f.height is not None else None, + "speed": float(f.speed) if f.speed is not None else None, + } + for f in rows + ] + + return BARUtils.success_exit(res) + + +@fastpheno.route("/bands/available/") +class FastPhenoBandsAvailable(Resource): + @fastpheno.param("flights_pk", _in="path", default=14) + def get(self, flights_pk): + """Returns all distinct band names available for a given flight.""" + if not BARUtils.is_integer(str(flights_pk)): + return BARUtils.error_exit("Invalid flights_pk"), 400 + + rows = db.session.execute( + db.select(Bands.band) + .where(Bands.flights_pk == flights_pk) + .distinct() + .order_by(db.func.cast(db.func.regexp_replace(Bands.band, "[^0-9]", ""), db.Integer)) + ).scalars().all() + + if len(rows) == 0: + return BARUtils.error_exit("No bands found for the given flight"), 400 + + return BARUtils.success_exit(rows) diff --git a/config/databases/fastpheno.sql b/config/databases/fastpheno.sql index dcf25dfc..340ee75e 100644 --- a/config/databases/fastpheno.sql +++ b/config/databases/fastpheno.sql @@ -19,122 +19,232 @@ -- Current Database: `fastpheno` -- -CREATE DATABASE /*!32312 IF NOT EXISTS*/ `fastpheno` /*!40100 DEFAULT CHARACTER SET latin1 */ /*!80016 DEFAULT ENCRYPTION='N' */; +CREATE DATABASE /*!32312 IF NOT EXISTS*/ `fastpheno` /*!40100 DEFAULT CHARACTER SET utf8mb4 */ /*!80016 DEFAULT ENCRYPTION='N' */; USE `fastpheno`; -- --- Table structure for table `band` +-- Table structure for table `sites` -- -DROP TABLE IF EXISTS `band`; +DROP TABLE IF EXISTS `sites`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `band` ( +CREATE TABLE `sites` ( + `sites_pk` int NOT NULL AUTO_INCREMENT, + `site_name` varchar(45) NOT NULL, + `lat` decimal(15,12) NOT NULL, + `lng` decimal(15,12) NOT NULL, + `site_desc` varchar(999) DEFAULT NULL, + PRIMARY KEY (`sites_pk`), + UNIQUE KEY `site_name` (`site_name`) +) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `sites` +-- + +LOCK TABLES `sites` WRITE; +/*!40000 ALTER TABLE `sites` DISABLE KEYS */; +INSERT INTO `sites` VALUES (1,'Pintendre',46.740374307111,-71.134103487947,'Pintendre site'),(2,'Pickering',43.976084584507,-79.155886757746,'Pickering site'); +/*!40000 ALTER TABLE `sites` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `flights` +-- + +DROP TABLE IF EXISTS `flights`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `flights` ( + `flights_pk` int NOT NULL AUTO_INCREMENT, + `pilot` varchar(45) DEFAULT NULL, + `flight_date` datetime NOT NULL, + `sites_pk` int NOT NULL, + `height` decimal(15,10) DEFAULT NULL, + `speed` decimal(15,10) DEFAULT NULL, + PRIMARY KEY (`flights_pk`), + KEY `flights_sites_fk_idx` (`sites_pk`), + CONSTRAINT `flights_sites_fk` FOREIGN KEY (`sites_pk`) REFERENCES `sites` (`sites_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB AUTO_INCREMENT=16 DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `flights` +-- + +LOCK TABLES `flights` WRITE; +/*!40000 ALTER TABLE `flights` DISABLE KEYS */; +INSERT INTO `flights` VALUES (14,NULL,'2022-06-10 00:00:00',1,NULL,NULL),(15,NULL,'2022-07-16 00:00:00',1,NULL,NULL); +/*!40000 ALTER TABLE `flights` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `trees` +-- + +DROP TABLE IF EXISTS `trees`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `trees` ( + `trees_pk` int NOT NULL AUTO_INCREMENT, + `sites_pk` int NOT NULL, + `longitude` decimal(15,12) NOT NULL, + `latitude` decimal(15,12) NOT NULL, + `tree_site_id` varchar(45) DEFAULT NULL, + `family_id` varchar(45) DEFAULT NULL, + `external_link` varchar(200) DEFAULT NULL, + `block_num` int(4) DEFAULT NULL, + `seq_id` varchar(25) DEFAULT NULL, + `x_pos` int(10) DEFAULT NULL, + `y_pos` int(10) DEFAULT NULL, + `height_2022` varchar(10) DEFAULT NULL, + PRIMARY KEY (`trees_pk`), + KEY `trees_sites_fk_idx` (`sites_pk`), + CONSTRAINT `trees_sites_fk` FOREIGN KEY (`sites_pk`) REFERENCES `sites` (`sites_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB AUTO_INCREMENT=11 DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `trees` +-- + +LOCK TABLES `trees` WRITE; +/*!40000 ALTER TABLE `trees` DISABLE KEYS */; +INSERT INTO `trees` VALUES +(1,1,-71.134606710951,46.740492838807,'c',NULL,NULL,6,'PIN_2547',38,68,'-1'), +(2,1,-71.134693444446,46.739706272423,'127.07',NULL,NULL,5,'PIN_1301',9,35,'210'), +(3,1,-71.133814526723,46.740219868520,'619.03',NULL,NULL,6,'PIN_2970',6,79,'273'), +(4,1,-71.133710686130,46.740917414015,'1217.06',NULL,NULL,7,'PIN_4135',31,109,'300'), +(5,1,-71.135473011332,46.739610902839,'2346.11',NULL,NULL,5,'PIN_355',26,10,'335'), +(6,1,-71.135569085909,46.739395807101,NULL,NULL,NULL,NULL,'PIN_6232',20,-1,NULL), +(7,1,-71.133666755146,46.740193293865,'c',NULL,NULL,6,'PIN_3116',1,82,'-1'), +(8,1,-71.133225418530,46.741142099373,'1756.12',NULL,NULL,7,'PIN_4967',27,131,'371'), +(9,1,-71.135196603859,46.739519816924,'1883.04',NULL,NULL,5,'PIN_518',15,14,'320'), +(10,1,-71.134888423241,46.740253827983,'2349.01',NULL,NULL,5,'PIN_1936',36,51,'358'), +(2260,1,-71.133110604530,46.741369741463,'619.02',NULL,NULL,7,'PIN_2260',33,143,'300'), +(5384,1,-71.134685695396,46.740188723522,'619.12',NULL,NULL,7,'PIN_2025',28,54,'410'); +/*!40000 ALTER TABLE `trees` ENABLE KEYS */; +UNLOCK TABLES; + +-- +-- Table structure for table `trees_flights_join_tbl` +-- + +DROP TABLE IF EXISTS `trees_flights_join_tbl`; +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!50503 SET character_set_client = utf8mb4 */; +CREATE TABLE `trees_flights_join_tbl` ( `trees_pk` int NOT NULL, - `month` enum('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec') NOT NULL, - `band` varchar(100) NOT NULL, - `value` decimal(20,15) NOT NULL, - PRIMARY KEY (`trees_pk`,`month`,`band`), - KEY `trees_fk_idx` (`trees_pk`), - CONSTRAINT `trees_fk` FOREIGN KEY (`trees_pk`) REFERENCES `trees` (`trees_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE=InnoDB DEFAULT CHARSET=latin1; + `flights_pk` int NOT NULL, + `confidence` decimal(8,5) DEFAULT NULL, + PRIMARY KEY (`trees_pk`,`flights_pk`), + KEY `tfjt_flights_fk_idx` (`flights_pk`), + CONSTRAINT `tfjt_trees_fk` FOREIGN KEY (`trees_pk`) REFERENCES `trees` (`trees_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT, + CONSTRAINT `tfjt_flights_fk` FOREIGN KEY (`flights_pk`) REFERENCES `flights` (`flights_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Dumping data for table `band` +-- Dumping data for table `trees_flights_join_tbl` -- -LOCK TABLES `band` WRITE; -/*!40000 ALTER TABLE `band` DISABLE KEYS */; -INSERT INTO `band` VALUES (1,'jan','band_1',0.025796278000000),(1,'jan','band_2',0.025796278000000),(1,'feb','band_1',0.025796278000000),(1,'mar','band_1',0.023442323224100),(1,'apr','band_1',0.089900613000000),(2,'feb','band_1',0.183586478000000),(4,'feb','band_1',0.223586478000000); -/*!40000 ALTER TABLE `band` ENABLE KEYS */; +LOCK TABLES `trees_flights_join_tbl` WRITE; +/*!40000 ALTER TABLE `trees_flights_join_tbl` DISABLE KEYS */; +INSERT INTO `trees_flights_join_tbl` VALUES (1,14,0.99769),(1,15,0.99823),(2,14,0.99831),(3,14,0.99856),(2260,14,0.99654),(5384,14,0.99840); +/*!40000 ALTER TABLE `trees_flights_join_tbl` ENABLE KEYS */; UNLOCK TABLES; -- --- Table structure for table `height` +-- Table structure for table `bands` -- -DROP TABLE IF EXISTS `height`; +DROP TABLE IF EXISTS `bands`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `height` ( +CREATE TABLE `bands` ( `trees_pk` int NOT NULL, - `month` enum('jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec') NOT NULL, - `tree_height_proxy` decimal(20,15) NOT NULL, - `ground_height_proxy` decimal(20,15) NOT NULL, - PRIMARY KEY (`trees_pk`,`month`), - KEY `tree_fk_idx` (`trees_pk`), - CONSTRAINT `tree_fk` FOREIGN KEY (`trees_pk`) REFERENCES `trees` (`trees_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE=InnoDB DEFAULT CHARSET=latin1; + `flights_pk` int NOT NULL, + `band` varchar(20) NOT NULL, + `value` decimal(8,5) NOT NULL, + PRIMARY KEY (`trees_pk`,`flights_pk`,`band`), + KEY `bands_flight_band_tree_idx` (`flights_pk`,`band`,`trees_pk`), + CONSTRAINT `bands_tfjt_fk` FOREIGN KEY (`trees_pk`,`flights_pk`) REFERENCES `trees_flights_join_tbl` (`trees_pk`,`flights_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Dumping data for table `height` +-- Dumping data for table `bands` -- -LOCK TABLES `height` WRITE; -/*!40000 ALTER TABLE `height` DISABLE KEYS */; -INSERT INTO `height` VALUES (1,'jan',2.234289428710000,45.106719970000000),(1,'feb',3.478942871000000,49.106719970000000),(1,'mar',2.383630037000000,48.887859340000000),(1,'apr',1.376412749000000,49.052417760000000),(2,'feb',2.383630037000000,48.123412421311630),(4,'feb',2.623630037000000,45.223412421311630); -/*!40000 ALTER TABLE `height` ENABLE KEYS */; +LOCK TABLES `bands` WRITE; +/*!40000 ALTER TABLE `bands` DISABLE KEYS */; +INSERT INTO `bands` VALUES +(1,14,'1002nm',1.52376),(1,14,'398nm',0.07344),(1,14,'400nm',0.07803),(1,14,'402nm',0.07608), +(1,14,'405nm',0.08176),(1,14,'407nm',0.07852),(1,14,'409nm',0.18621),(1,14,'411nm',0.07256), +(1,14,'414nm',0.08194),(1,14,'416nm',0.07675), +(1,15,'1002nm',0.81632),(1,15,'398nm',0.04504),(1,15,'400nm',0.04862),(1,15,'402nm',0.05335), +(1,15,'405nm',0.05336),(1,15,'407nm',0.05417),(1,15,'409nm',0.10724),(1,15,'411nm',0.04718), +(1,15,'414nm',0.05260),(1,15,'416nm',0.04857), +(2,14,'398nm',0.03096),(3,14,'398nm',0.03606), +(2260,14,'398nm',0.03531),(5384,14,'398nm',0.03913); +/*!40000 ALTER TABLE `bands` ENABLE KEYS */; UNLOCK TABLES; -- --- Table structure for table `sites` +-- Table structure for table `pigments` -- -DROP TABLE IF EXISTS `sites`; +DROP TABLE IF EXISTS `pigments`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `sites` ( - `sites_pk` int NOT NULL AUTO_INCREMENT, - `site_name` varchar(45) NOT NULL, - `site_desc` varchar(999) DEFAULT NULL, - PRIMARY KEY (`sites_pk`), - UNIQUE KEY `site_name` (`site_name`) -) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=latin1; +CREATE TABLE `pigments` ( + `trees_pk` int NOT NULL, + `flights_pk` int NOT NULL, + `pigment` int NOT NULL, + `value` decimal(20,15) NOT NULL, + PRIMARY KEY (`trees_pk`,`flights_pk`,`pigment`), + CONSTRAINT `pigments_tfjt_fk` FOREIGN KEY (`trees_pk`,`flights_pk`) REFERENCES `trees_flights_join_tbl` (`trees_pk`,`flights_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Dumping data for table `sites` +-- Dumping data for table `pigments` -- -LOCK TABLES `sites` WRITE; -/*!40000 ALTER TABLE `sites` DISABLE KEYS */; -INSERT INTO `sites` VALUES (1,'Pintendre','Lorem ipsum dolor sit amet, consectetur adipiscing elit'),(2,'Pickering','Lorem ipsum dolor sit amet,'); -/*!40000 ALTER TABLE `sites` ENABLE KEYS */; +LOCK TABLES `pigments` WRITE; +/*!40000 ALTER TABLE `pigments` DISABLE KEYS */; +/*!40000 ALTER TABLE `pigments` ENABLE KEYS */; UNLOCK TABLES; -- --- Table structure for table `trees` +-- Table structure for table `unispec` -- -DROP TABLE IF EXISTS `trees`; +DROP TABLE IF EXISTS `unispec`; /*!40101 SET @saved_cs_client = @@character_set_client */; /*!50503 SET character_set_client = utf8mb4 */; -CREATE TABLE `trees` ( - `trees_pk` int NOT NULL AUTO_INCREMENT, - `sites_pk` int NOT NULL, - `longitude` decimal(10,0) NOT NULL, - `latitude` decimal(10,0) NOT NULL, - `genotype_id` varchar(5) DEFAULT NULL, - `external_link` varchar(200) DEFAULT NULL, - `tree_given_id` varchar(25) DEFAULT NULL, - PRIMARY KEY (`trees_pk`), - KEY `sites_fk_idx` (`sites_pk`), - CONSTRAINT `sites_fk` FOREIGN KEY (`sites_pk`) REFERENCES `sites` (`sites_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT -) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=latin1; +CREATE TABLE `unispec` ( + `trees_pk` int NOT NULL, + `flights_pk` int NOT NULL, + `pigment` int NOT NULL, + `value` decimal(20,15) NOT NULL, + PRIMARY KEY (`trees_pk`,`flights_pk`,`pigment`), + CONSTRAINT `unispec_tfjt_fk` FOREIGN KEY (`trees_pk`,`flights_pk`) REFERENCES `trees_flights_join_tbl` (`trees_pk`,`flights_pk`) ON DELETE RESTRICT ON UPDATE RESTRICT +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; /*!40101 SET character_set_client = @saved_cs_client */; -- --- Dumping data for table `trees` +-- Dumping data for table `unispec` -- -LOCK TABLES `trees` WRITE; -/*!40000 ALTER TABLE `trees` DISABLE KEYS */; -INSERT INTO `trees` VALUES (1,1,336839,5178557,'C','example','11'),(2,1,336872,5178486,'C','example2','11'),(3,1,346872,5278486,'C','example3','B'),(4,2,330502,5262486,'XZ','example4','K123'); -/*!40000 ALTER TABLE `trees` ENABLE KEYS */; +LOCK TABLES `unispec` WRITE; +/*!40000 ALTER TABLE `unispec` DISABLE KEYS */; +/*!40000 ALTER TABLE `unispec` ENABLE KEYS */; UNLOCK TABLES; + /*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */; /*!40101 SET SQL_MODE=@OLD_SQL_MODE */; @@ -145,4 +255,4 @@ UNLOCK TABLES; /*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */; /*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */; --- Dump completed on 2024-07-29 11:30:25 +-- Dump completed on 2026-03-16 00:00:00 diff --git a/tests/resources/test_fastpheno.py b/tests/resources/test_fastpheno.py index e61bc97c..ea5d04b4 100644 --- a/tests/resources/test_fastpheno.py +++ b/tests/resources/test_fastpheno.py @@ -6,127 +6,294 @@ class TestIntegrations(TestCase): def setUp(self): self.app_client = app.test_client() - def test_bands(self): - """This function checks GET request for fastpheno bands - :return: - """ - response = self.app_client.get("/fastpheno/get_bands/pintendre/feb/band_1") + def test_get_bands(self): + """Tests GET /fastpheno/get_bands///""" + response = self.app_client.get("/fastpheno/get_bands/Pintendre/14/398nm") expected = { "wasSuccessful": True, "data": [ { - "site_name": "Pintendre", - "tree_id": 1, - "longitude": 336839, - "latitutde": 5178557, - "genotype_id": "C", - "tree_given_id": "11", - "external_link": "example", - "band_value": 0.025796278, - "tree_height_proxy": 3.478942871, - "ground_height_proxy": 49.10671997, - "band_month": "feb", + "trees_pk": 1, + "seq_id": "PIN_2547", + "longitude": -71.134606710951, + "latitude": 46.740492838807, + "x_pos": 38, + "y_pos": 68, + "height_2022": "-1", + "block_num": 6, + "tree_site_id": "c", + "confidence": 0.99769, + "band_value": 0.07344, }, { - "site_name": "Pintendre", - "tree_id": 2, - "longitude": 336872, - "latitutde": 5178486, - "genotype_id": "C", - "tree_given_id": "11", - "external_link": "example2", - "band_value": 0.183586478, - "tree_height_proxy": 2.383630037, - "ground_height_proxy": 48.12341242131163, - "band_month": "feb", + "trees_pk": 2, + "seq_id": "PIN_1301", + "longitude": -71.134693444446, + "latitude": 46.739706272423, + "x_pos": 9, + "y_pos": 35, + "height_2022": "210", + "block_num": 5, + "tree_site_id": "127.07", + "confidence": 0.99831, + "band_value": 0.03096, + }, + { + "trees_pk": 3, + "seq_id": "PIN_2970", + "longitude": -71.133814526723, + "latitude": 46.74021986852, + "x_pos": 6, + "y_pos": 79, + "height_2022": "273", + "block_num": 6, + "tree_site_id": "619.03", + "confidence": 0.99856, + "band_value": 0.03606, + }, + { + "trees_pk": 2260, + "seq_id": "PIN_2260", + "longitude": -71.13311060453, + "latitude": 46.741369741463, + "x_pos": 33, + "y_pos": 143, + "height_2022": "300", + "block_num": 7, + "tree_site_id": "619.02", + "confidence": 0.99654, + "band_value": 0.03531, + }, + { + "trees_pk": 5384, + "seq_id": "PIN_2025", + "longitude": -71.134685695396, + "latitude": 46.740188723522, + "x_pos": 28, + "y_pos": 54, + "height_2022": "410", + "block_num": 7, + "tree_site_id": "619.12", + "confidence": 0.9984, + "band_value": 0.03913, }, ], } self.assertEqual(response.json, expected) - # Not working version - response = self.app_client.get("/fastpheno/get_bands/NOTASITE/feb/band_1") - expected = { - "wasSuccessful": False, - "error": "There are no data found for the given parameters", - } + # No data for valid params + response = self.app_client.get("/fastpheno/get_bands/Pickering/14/398nm") + expected = {"wasSuccessful": False, "error": "No data found for the given parameters"} self.assertEqual(response.json, expected) - # Invalid site - response = self.app_client.get("/fastpheno/get_bands/12345/feb/band_1") + # Invalid site name (numeric) + response = self.app_client.get("/fastpheno/get_bands/12345/14/398nm") + expected = {"wasSuccessful": False, "error": "Invalid site name"} + self.assertEqual(response.json, expected) + + # Invalid flight ID (not an integer) + response = self.app_client.get("/fastpheno/get_bands/Pintendre/abc/398nm") + expected = {"wasSuccessful": False, "error": "Invalid flight ID"} + self.assertEqual(response.json, expected) + + # Invalid band (special characters) + response = self.app_client.get("/fastpheno/get_bands/Pintendre/14/398!nm") + expected = {"wasSuccessful": False, "error": "Invalid band"} + self.assertEqual(response.json, expected) + + def test_get_trees(self): + """Tests GET /fastpheno/get_trees/""" + response = self.app_client.get("/fastpheno/get_trees/619") expected = { - "wasSuccessful": False, - "error": "Invalid site name", + "wasSuccessful": True, + "data": [ + { + "site_name": "Pintendre", + "trees_pk": 3, + "seq_id": "PIN_2970", + "longitude": -71.133814526723, + "latitude": 46.74021986852, + "tree_site_id": "619.03", + "external_link": None, + }, + { + "site_name": "Pintendre", + "trees_pk": 2260, + "seq_id": "PIN_2260", + "longitude": -71.13311060453, + "latitude": 46.741369741463, + "tree_site_id": "619.02", + "external_link": None, + }, + { + "site_name": "Pintendre", + "trees_pk": 5384, + "seq_id": "PIN_2025", + "longitude": -71.134685695396, + "latitude": 46.740188723522, + "tree_site_id": "619.12", + "external_link": None, + }, + ], } self.assertEqual(response.json, expected) - # Invalid month - response = self.app_client.get("/fastpheno/get_bands/pintendre/1234/band_1") + # With optional site filter + response = self.app_client.get("/fastpheno/get_trees/619?site=Pintendre") + self.assertEqual(response.json, expected) + + # No data for valid genotype not in DB + response = self.app_client.get("/fastpheno/get_trees/9999") + expected = {"wasSuccessful": False, "error": "No data found for the given parameters"} + self.assertEqual(response.json, expected) + + # No data for valid genotype but wrong site + response = self.app_client.get("/fastpheno/get_trees/619?site=Pickering") + expected = {"wasSuccessful": False, "error": "No data found for the given parameters"} + self.assertEqual(response.json, expected) + + # Invalid tree_site_id (too long) + response = self.app_client.get("/fastpheno/get_trees/TOOLONGID12345") + expected = {"wasSuccessful": False, "error": "Invalid tree site ID"} + self.assertEqual(response.json, expected) + + # Invalid site param (special characters) + response = self.app_client.get("/fastpheno/get_trees/619?site=123") + expected = {"wasSuccessful": False, "error": "Invalid site name"} + self.assertEqual(response.json, expected) + + def test_timeseries_tree(self): + """Tests GET /fastpheno/timeseries/tree//""" + response = self.app_client.get("/fastpheno/timeseries/tree/PIN_2547/398nm") expected = { - "wasSuccessful": False, - "error": "Invalid month", + "wasSuccessful": True, + "data": [ + { + "flight_date": "2022-06-10T00:00:00", + "flights_pk": 14, + "confidence": 0.99769, + "band_value": 0.07344, + }, + { + "flight_date": "2022-07-16T00:00:00", + "flights_pk": 15, + "confidence": 0.99823, + "band_value": 0.04504, + }, + ], } self.assertEqual(response.json, expected) + # No data for valid tree with no band match + response = self.app_client.get("/fastpheno/timeseries/tree/PIN_2547/999nm") + expected = {"wasSuccessful": False, "error": "No data found for the given parameters"} + self.assertEqual(response.json, expected) + + # Invalid seq_id format + response = self.app_client.get("/fastpheno/timeseries/tree/NOTVALID/398nm") + expected = {"wasSuccessful": False, "error": "Invalid seq_id"} + self.assertEqual(response.json, expected) + # Invalid band - response = self.app_client.get("/fastpheno/get_bands/NOTASITE/feb/band_x") - expected = { - "wasSuccessful": False, - "error": "Invalid band", - } + response = self.app_client.get("/fastpheno/timeseries/tree/PIN_2547/398!nm") + expected = {"wasSuccessful": False, "error": "Invalid band"} self.assertEqual(response.json, expected) - def test_site_genotype_ids(self): - """This function checks GET request for fastpheno sites for genotype_ids - :return: - """ - response = self.app_client.get("/fastpheno/get_trees/C") + def test_timeseries_genotype_aggregate(self): + """Tests GET /fastpheno/timeseries/genotype///aggregate""" + response = self.app_client.get("/fastpheno/timeseries/genotype/619/398nm/aggregate") expected = { "wasSuccessful": True, "data": [ { - "site_name": "Pintendre", - "tree_id": 1, - "longitude": 336839, - "latitutde": 5178557, - "genotype_id": "C", - "tree_given_id": "11", - "external_link": "example", + "flight_date": "2022-06-10T00:00:00", + "flights_pk": 14, + "avg_value": 0.036833333, + "std_value": 0.0016526006441027672, + "n_trees": 3, }, + ], + } + self.assertEqual(response.json, expected) + + # No data for valid but non-existent genotype + response = self.app_client.get("/fastpheno/timeseries/genotype/9999/398nm/aggregate") + expected = {"wasSuccessful": False, "error": "No data found for the given parameters"} + self.assertEqual(response.json, expected) + + # Invalid tree_site_id (too long) + response = self.app_client.get("/fastpheno/timeseries/genotype/TOOLONGID123/398nm/aggregate") + expected = {"wasSuccessful": False, "error": "Invalid tree site ID"} + self.assertEqual(response.json, expected) + + # Invalid band + response = self.app_client.get("/fastpheno/timeseries/genotype/619/398!nm/aggregate") + expected = {"wasSuccessful": False, "error": "Invalid band"} + self.assertEqual(response.json, expected) + + def test_sites(self): + """Tests GET /fastpheno/sites""" + response = self.app_client.get("/fastpheno/sites") + expected = { + "wasSuccessful": True, + "data": [ { - "site_name": "Pintendre", - "tree_id": 2, - "longitude": 336872, - "latitutde": 5178486, - "genotype_id": "C", - "tree_given_id": "11", - "external_link": "example2", + "sites_pk": 2, + "site_name": "Pickering", + "lat": 43.976084584507, + "lng": -79.155886757746, + "site_desc": "Pickering site", }, { + "sites_pk": 1, "site_name": "Pintendre", - "tree_id": 3, - "longitude": 346872, - "latitutde": 5278486, - "genotype_id": "C", - "tree_given_id": "B", - "external_link": "example3", + "lat": 46.740374307111, + "lng": -71.134103487947, + "site_desc": "Pintendre site", }, ], } self.assertEqual(response.json, expected) - # Not working version - response = self.app_client.get("/fastpheno/get_trees/Z") + def test_flights(self): + """Tests GET /fastpheno/flights/""" + response = self.app_client.get("/fastpheno/flights/1") expected = { - "wasSuccessful": False, - "error": "There are no data found for the given parameters", + "wasSuccessful": True, + "data": [ + { + "flights_pk": 14, + "flight_date": "2022-06-10T00:00:00", + "pilot": None, + "height": None, + "speed": None, + }, + { + "flights_pk": 15, + "flight_date": "2022-07-16T00:00:00", + "pilot": None, + "height": None, + "speed": None, + }, + ], } self.assertEqual(response.json, expected) - # Invalid data - response = self.app_client.get("/fastpheno/get_trees/NOTVALID") + # No flights for a valid but empty site + response = self.app_client.get("/fastpheno/flights/2") + expected = {"wasSuccessful": False, "error": "No flights found for the given site"} + self.assertEqual(response.json, expected) + + def test_bands_available(self): + """Tests GET /fastpheno/bands/available/""" + response = self.app_client.get("/fastpheno/bands/available/14") expected = { - "wasSuccessful": False, - "error": "Invalid genotype id", + "wasSuccessful": True, + "data": ["398nm", "400nm", "402nm", "405nm", "407nm", "409nm", "411nm", "414nm", "416nm", "1002nm"], } self.assertEqual(response.json, expected) + + # No bands for a non-existent flight + response = self.app_client.get("/fastpheno/bands/available/999") + expected = {"wasSuccessful": False, "error": "No bands found for the given flight"} + self.assertEqual(response.json, expected)