Skip to content
Snippets Groups Projects
test_data_api.py 12.11 KiB
import json
import unittest
from unittest import mock

import metobsapi


def fake_data(interval, symbols, num_vals, single_result=False):
    import random
    from datetime import datetime, timedelta

    from influxdb.resultset import ResultSet

    now = datetime(2017, 3, 5, 19, 0, 0)
    t_format = "%Y-%m-%dT%H:%M:%SZ"
    measurement_name = "metobs_" + interval
    series = []
    for (site, inst), columns in symbols.items():
        tags = {"site": site, "inst": inst}
        vals = []
        for i in range(num_vals):
            vals.append([(now + timedelta(minutes=i)).strftime(t_format)] + [random.random()] * (len(columns) - 1))
            # make up some Nones/nulls (but not all the time)
            r_idx = int(random.random() * len(columns) * 3)
            # don't include time (index 0)
            if 0 < r_idx < len(columns):
                vals[-1][r_idx] = None
        s = {
            "name": measurement_name,
            "columns": columns,
            "tags": tags,
            "values": vals,
        }
        if single_result:
            series.append(s)
        else:
            series.append(
                ResultSet(
                    {
                        "series": [s],
                        "statement_id": 0,
                    }
                )
            )

    if single_result:
        ret = {
            "series": series,
            "statement_id": 0,
        }
        return ResultSet(ret)
    else:
        return series


class TestDataAPI(unittest.TestCase):
    def setUp(self):
        metobsapi.app.config["TESTING"] = True
        metobsapi.app.config["DEBUG"] = True
        self.app = metobsapi.app.test_client()

    def test_doc(self):
        res = self.app.get("/api/data")
        assert b"Data Request Application" in res.data

    def test_bad_format(self):
        res = self.app.get("/api/data.fake")
        self.assertIn(b"No data file format", res.data)

    def test_bad_begin_json(self):
        res = self.app.get("/api/data.json?symbols=air_temp&begin=blah")
        res = json.loads(res.data.decode())
        self.assertEqual(res["code"], 400)
        self.assertEqual(res["status"], "error")
        self.assertIn("timestamp", res["message"])

    def test_bad_order(self):
        res = self.app.get("/api/data.json?order=blah&symbols=air_temp")
        res = json.loads(res.data.decode())
        self.assertIn("column", res["message"])
        self.assertIn("row", res["message"])

    def test_bad_epoch(self):
        res = self.app.get("/api/data.json?epoch=blah&symbols=air_temp")
        res = json.loads(res.data.decode())
        self.assertIn("'h'", res["message"])
        self.assertIn("'m'", res["message"])
        self.assertIn("'s'", res["message"])
        self.assertIn("'u'", res["message"])

    def test_bad_interval(self):
        res = self.app.get("/api/data.json?interval=blah&symbols=air_temp")
        res = json.loads(res.data.decode())
        self.assertIn("'1m'", res["message"])
        self.assertIn("'5m'", res["message"])
        self.assertIn("'1h'", res["message"])

    def test_missing_inst(self):
        res = self.app.get("/api/data.json?site=X&symbols=air_temp&begin=-05:00:00")
        res = json.loads(res.data.decode())
        self.assertEqual(res["code"], 400)
        self.assertEqual(res["status"], "error")
        self.assertIn("'site'", res["message"])
        self.assertIn("'inst'", res["message"])

    def test_missing_site(self):
        res = self.app.get("/api/data.json?inst=X&symbols=air_temp&begin=-05:00:00")
        res = json.loads(res.data.decode())
        self.assertEqual(res["code"], 400)
        self.assertEqual(res["status"], "error")
        self.assertIn("'site'", res["message"])
        self.assertIn("'inst'", res["message"])

    def test_missing_symbols(self):
        res = self.app.get("/api/data.json?begin=-05:00:00")
        res = json.loads(res.data.decode())
        self.assertEqual(res["code"], 400)
        self.assertEqual(res["status"], "error")
        self.assertIn("'symbols'", res["message"])

    def test_too_many_points(self):
        res = self.app.get("/api/data.json?symbols=aoss.tower.air_temp&begin=1970-01-01T00:00:00")
        self.assertEqual(res.status_code, 413)
        res = json.loads(res.data.decode())
        self.assertIn("too many values", res["message"])
        self.assertEqual(res["code"], 413)
        self.assertEqual(res["status"], "fail")

    @mock.patch("metobsapi.data_api.query")
    def test_shorthand_one_symbol_json_row(self, query_func):
        r = fake_data("1m", {("aoss", "tower"): ["time", "air_temp"]}, 9)
        query_func.return_value = r
        # row should be the default
        res = self.app.get("/api/data.json?site=aoss&inst=tower&symbols=air_temp&begin=-00:10:00")
        res = json.loads(res.data.decode())
        self.assertEqual(res["code"], 200)
        self.assertEqual(res["num_results"], 9)
        self.assertListEqual(res["results"]["symbols"], ["air_temp"])
        self.assertEqual(len(res["results"]["timestamps"]), 9)
        self.assertEqual(len(res["results"]["data"]), 9)
        self.assertEqual(len(res["results"]["data"][0]), 1)

    @mock.patch("metobsapi.data_api.query")
    def test_shorthand_one_symbol_json_column(self, query_func):
        r = fake_data("1m", {("aoss", "tower"): ["time", "air_temp"]}, 9)
        query_func.return_value = r
        res = self.app.get("/api/data.json?site=aoss&inst=tower&symbols=air_temp&begin=-00:10:00&order=column")
        res = json.loads(res.data.decode())
        self.assertEqual(res["code"], 200)
        self.assertEqual(res["num_results"], 9)
        self.assertIn("air_temp", res["results"]["data"])
        self.assertEqual(len(res["results"]["data"]["air_temp"]), 9)
        self.assertEqual(len(res["results"]["timestamps"]), 9)

    @mock.patch("metobsapi.data_api.query")
    def test_wind_speed_direction_json(self, query_func):
        r = fake_data("1m", {("aoss", "tower"): ["time", "wind_speed", "wind_direction", "wind_east", "wind_north"]}, 9)
        query_func.return_value = r
        res = self.app.get(
            "/api/data.json?symbols=aoss.tower.wind_speed:aoss.tower.wind_direction&begin=-00:10:00&order=column"
        )
        res = json.loads(res.data.decode())
        self.assertEqual(res["code"], 200)
        self.assertEqual(res["num_results"], 9)
        self.assertIn("aoss.tower.wind_direction", res["results"]["data"])
        self.assertIn("aoss.tower.wind_speed", res["results"]["data"])
        self.assertEqual(len(list(res["results"]["data"].keys())), 2)

    @mock.patch("metobsapi.data_api.query")
    def test_one_symbol_two_insts_json_row(self, query_func):
        r = fake_data(
            "1m",
            {
                ("aoss", "tower"): ["time", "air_temp"],
                ("mendota", "buoy"): ["time", "air_temp"],
            },
            9,
        )
        query_func.return_value = r
        # row should be the default
        res = self.app.get("/api/data.json?symbols=aoss.tower.air_temp:mendota.buoy.air_temp&begin=-00:10:00")
        res = json.loads(res.data.decode())
        self.assertEqual(res["code"], 200)
        self.assertEqual(res["num_results"], 9)
        self.assertListEqual(res["results"]["symbols"], ["aoss.tower.air_temp", "mendota.buoy.air_temp"])
        self.assertEqual(len(res["results"]["timestamps"]), 9)
        self.assertEqual(len(res["results"]["data"]), 9)
        self.assertEqual(len(res["results"]["data"][0]), 2)

    @mock.patch("metobsapi.data_api.query")
    def test_one_symbol_three_insts_json_row(self, query_func):
        r = fake_data(
            "1m",
            {
                ("site1", "inst1"): ["time", "air_temp"],
                ("site2", "inst2"): ["time", "air_temp"],
                ("site3", "inst3"): ["time", "air_temp"],
            },
            9,
        )
        query_func.return_value = r
        # row should be the default
        from metobsapi.util.data_responses import SYMBOL_TRANSLATIONS as st

        st = st.copy()
        st[("site1", "inst1")] = st[("aoss", "tower")]
        st[("site2", "inst2")] = st[("aoss", "tower")]
        st[("site3", "inst3")] = st[("aoss", "tower")]
        with mock.patch("metobsapi.util.data_responses.SYMBOL_TRANSLATIONS", st):
            res = self.app.get(
                "/api/data.json?symbols=site1.inst1.air_temp:site2.inst2.air_temp:site3.inst3.air_temp&begin=-00:10:00"
            )
            res = json.loads(res.data.decode())
            self.assertEqual(res["code"], 200)
            self.assertEqual(res["num_results"], 9)
            self.assertListEqual(
                res["results"]["symbols"], ["site1.inst1.air_temp", "site2.inst2.air_temp", "site3.inst3.air_temp"]
            )
            self.assertEqual(len(res["results"]["timestamps"]), 9)
            self.assertEqual(len(res["results"]["data"]), 9)
            self.assertEqual(len(res["results"]["data"][0]), 3)

    @mock.patch("metobsapi.data_api.query")
    def test_one_symbol_csv(self, query_func):
        r = fake_data("1m", {("aoss", "tower"): ["time", "air_temp"]}, 9)
        query_func.return_value = r
        # row should be the default
        res = self.app.get("/api/data.csv?symbols=aoss.tower.air_temp&begin=-00:10:00")
        res = res.data.decode()
        # header, data, newline at end
        lines = res.split("\n")
        self.assertEqual(len(lines), 5 + 9 + 1)
        # time + 1 channel
        self.assertEqual(len(lines[5].split(",")), 2)
        self.assertIn("# code: 200", res)

    @mock.patch("metobsapi.data_api.query")
    def test_one_symbol_xml(self, query_func):
        from xml.dom.minidom import parseString

        r = fake_data("1m", {("aoss", "tower"): ["time", "air_temp"]}, 9)
        query_func.return_value = r
        # row should be the default
        res = self.app.get("/api/data.xml?symbols=aoss.tower.air_temp&begin=-00:10:00")
        res = parseString(res.data.decode())
        # symbols: time and air_temp
        self.assertEqual(len(res.childNodes[0].childNodes[0].childNodes), 2)
        # data rows
        self.assertEqual(len(res.childNodes[0].childNodes[1].childNodes), 9)

    @mock.patch("metobsapi.data_api.query")
    def test_three_symbol_csv(self, query_func):
        """Test that multiple channels in a CSV file are structured properly."""
        r = fake_data("1m", {("aoss", "tower"): ["time", "air_temp", "rel_hum", "wind_speed"]}, 9)
        query_func.return_value = r
        # row should be the default
        res = self.app.get(
            "/api/data.csv?symbols=aoss.tower.air_temp:" "aoss.tower.rel_hum:aoss.tower.wind_speed&begin=-00:10:00"
        )
        res = res.data.decode()
        # header, data, newline at end
        lines = res.split("\n")
        self.assertEqual(len(lines), 5 + 9 + 1)
        # time + 3 channels
        self.assertEqual(len(lines[5].split(",")), 4)
        self.assertIn("# code: 200", res)

    @mock.patch("metobsapi.data_api.query")
    def test_three_symbol_csv_repeat(self, query_func):
        """Test that multiple channels in a CSV file are structured properly."""
        r = fake_data("1m", {("aoss", "tower"): ["time", "air_temp", "rel_hum", "wind_speed"]}, 9)
        query_func.return_value = r
        # row should be the default
        res = self.app.get(
            "/api/data.csv?symbols=aoss.tower.air_temp:" "aoss.tower.air_temp:aoss.tower.air_temp&begin=-00:10:00"
        )
        res = res.data.decode()
        # header, data, newline at end
        lines = res.split("\n")
        # header, data (one empty line), newline at end
        self.assertEqual(len(lines), 5 + 1 + 1)
        # time + 1 channel
        self.assertEqual(len(lines[5].split(",")), 1)
        self.assertIn("# code: 400", res)

    # @mock.patch('metobsapi.data_api.query')
    # def test_jsonp_bad_symbol_400(self, query_func):
    #     XXX: Not currently possible with flask-json
    #     r = fake_data('1m', {('aoss', 'tower'): ['time', 'air_temp']}, 9)
    #     query_func.return_value = r
    #     # row should be the default
    #     res = self.app.get('/api/data.json?site=aoss&inst=tower&symbols=bad&begin=-00:10:00&callback=test')
    #     self.assertEqual(res.status_code, 400)
    #     res = res.data.decode()
    #     self.assertEqual(res['code'], 400)