From 2bf486421d5060aac82cfcc616685e05f066d80a Mon Sep 17 00:00:00 2001 From: Nikki Tebaldi <17799906+nikki-t@users.noreply.github.com> Date: Wed, 8 May 2024 16:46:40 -0400 Subject: [PATCH] issues/101: Support for HTTP Accept header (#172) * Reorganize timeseries code to prep for Accept header * Enable Accept header to return response of specific content-type * Fix whitespace and string continuation * Make error handling consistent and add an additional test where a reach can't be found * Update changelog with issue for unreleased version * Add 415 status code to API definition * Few minor cleanup items * Few minor cleanup items * Update to aiohttp@3.9.4 * Fix dependencies --------- Co-authored-by: Frank Greguska <89428916+frankinspace@users.noreply.github.com> --- CHANGELOG.md | 1 + hydrocron/api/controllers/timeseries.py | 376 ++++-- hydrocron/api/swagger/swagger.yaml | 111 -- poetry.lock | 15 +- pyproject.toml | 1 + .../hydrocron_aws_api.yml | 34 +- tests/test_api.py | 522 ++++---- tests/test_data/api_query_results_csv.csv | 2 + .../test_data/api_query_results_geojson.json | 1147 +++++++++++++++++ 9 files changed, 1693 insertions(+), 516 deletions(-) delete mode 100644 hydrocron/api/swagger/swagger.yaml create mode 100644 tests/test_data/api_query_results_csv.csv create mode 100644 tests/test_data/api_query_results_geojson.json diff --git a/CHANGELOG.md b/CHANGELOG.md index 948a34e2..83c3a2f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### Added + - Issue 101 - Add support for HTTP Accept header ### Deprecated ### Removed ### Fixed diff --git a/hydrocron/api/controllers/timeseries.py b/hydrocron/api/controllers/timeseries.py index dacd83f4..5f62f0f0 100644 --- a/hydrocron/api/controllers/timeseries.py +++ b/hydrocron/api/controllers/timeseries.py @@ -9,6 +9,7 @@ import sys import time +from accept_types import get_best_match import pandas as pd import geopandas as gpd from shapely.wkt import loads @@ -21,12 +22,205 @@ logging.getLogger().setLevel(logging.INFO) +ACCEPT_TYPES = ['application/json', 'text/csv', 'application/geo+json'] + + class RequestError(Exception): """ Exception thrown if there is an error encoutered with request """ +def get_request_headers(event): + """Return request headers from event object. + + :param event: Request data dictionary + :type event: dict + + :rtype: dict + """ + + headers = {} + try: + headers['user_agent'] = event['headers']['User-Agent'] + headers['user_ip'] = event['headers']['X-Forwarded-For'].split(',')[0] + headers['accept'] = '*/*' if 'Accept' not in event['headers'].keys() else event['headers']['Accept'] + except KeyError as e: + logging.error('Error encountered with headers: %s', e) + raise RequestError(f'400: Issue encountered with request header: {e}') from e + return headers + + +def get_request_parameters(event): + """Return request parameters from event object. + + :param event: Request data dictionary + :type event: dict + + :rtype: dict + """ + + try: + feature = event['body']['feature'] + feature_id = event['body']['feature_id'] + start_time = event['body']['start_time'] + end_time = event['body']['end_time'] + output = 'default' if 'output' not in event['body'].keys() else event['body']['output'] + fields = event['body']['fields'] + except KeyError as e: + logging.error('Error encountered with request parameters: %s', e) + raise RequestError(f'400: This required parameter is missing: {e}') from e + + parameters, error_message = validate_parameters(feature, feature_id, start_time, end_time, output, fields) + if error_message: + raise RequestError(error_message) + + return parameters + + +def get_return_type(accept_header, output): + """Determine return type and output value requested by user from Accept header + + :param accept_header: Accept request header + :type accept_header: str + + :param output: Output type requested by user + :type output: str + + :rtype: str, str + """ + + return_type = get_best_match(accept_header, ACCEPT_TYPES) + + if return_type is None: + raise RequestError(f'415: Unsupported media type in Accept request header: {accept_header}. Hydrocron ' + f'only supports the following media types: {ACCEPT_TYPES}') + + if output != 'default': + if return_type != 'application/json': + logging.error('Error encountered with request Accept header: %s and output: %s', return_type, output) + raise RequestError(f'400: Invalid combination of Accept header ({accept_header}) and ' + + f'output request parameter ({output}). Remove output request parameter when ' + + 'requesting application/geo+json or text/csv') + + else: + if return_type in ('application/json', 'application/geo+json'): + output = 'geojson' + elif return_type == 'text/csv': + output = 'csv' + + return return_type, output + + +def validate_parameters(feature, feature_id, start_time, end_time, output, fields): + """ + Determine if all parameters are present and in the correct format. Return 400 + Bad Request if any errors are found alongside 0 hits. + + :param feature: Data requested for Reach or Node or Lake + :type feature: str + :param feature_id: ID of the feature to retrieve + :type feature_id: str + :param start_time: Start time of the timeseries + :type start_time: str + :param end_time: End time of the timeseries + :type end_time: str + :param output: Format of the data returned + :type output: str + :param fields: List of requested columns + :type fields: str + + :rtype: dict + """ + + parameters = {} + error_message = '' + + if feature not in ('Node', 'Reach'): + error_message = f'400: feature parameter should be Reach or Node, not: {feature}' + + elif not feature_id.isdigit(): + error_message = f'400: feature_id cannot contain letters: {feature_id}' + + elif not is_date_valid(start_time) or not is_date_valid(end_time): + error_message = ('400: start_time and end_time parameters must conform ' + 'to format: YYYY-MM-DDTHH:MM:SSZ or YYYY-MM-DDTHH:MM:SS-00:00') + + elif output not in ('csv', 'geojson', 'default'): + error_message = f'400: output parameter should be csv or geojson, not: {output}' + + elif not is_fields_valid(feature, fields): + error_message = '400: fields parameter should contain valid SWOT fields' + + else: + parameters['feature'] = feature + parameters['feature_id'] = feature_id + start_time, end_time = sanitize_time(start_time, end_time) + parameters['start_time'] = start_time + parameters['end_time'] = end_time + parameters['output'] = output + parameters['fields'] = fields + + return parameters, error_message + + +def is_date_valid(query_date): + """ + Check if the query date conforms to the correct format. + + :param query_date: Start or end time of the timeseries + :type query_date: str + + :rtype: bool + """ + + try: + datetime.datetime.strptime(query_date, "%Y-%m-%dT%H:%M:%S%z") + return True + except ValueError: + return False + + +def is_fields_valid(feature, fields): + """ + Check if fields are present in either the reach or node list of columns + + :param feature: The type of feature, either 'Reach' or 'Node' + :type feature: str + + :param fields: List of requested columns + :type fields: str + + :rtype: bool + """ + + fields = fields.split(',') + if feature == 'Reach': + columns = constants.REACH_ALL_COLUMNS + elif feature == 'Node': + columns = constants.NODE_ALL_COLUMNS + else: + columns = [] + return all(field in columns for field in fields) + + +def sanitize_time(start_time, end_time): + """ + Return formatted string to handle cases where request includes non-padded numbers + + :param start_time: Start time of the timeseries + :type start_time: str + :param end_time: End time of the timeseries + :type end_time: str + + :rtype: str, str + """ + + start_time = datetime.datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%dT%H:%M:%S%z") + end_time = datetime.datetime.strptime(end_time, "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%dT%H:%M:%S%z") + return start_time, end_time + + def timeseries_get(feature, feature_id, start_time, end_time, output, fields): # noqa: E501 """Get Timeseries for a particular Reach, Node, or LakeID @@ -43,7 +237,7 @@ def timeseries_get(feature, feature_id, start_time, end_time, output, fields): :param output: Format of the data returned :type output: str :param fields: List of requested columns - :type fields: dict + :type fields: str :rtype: Dict, integer """ @@ -96,7 +290,7 @@ def format_json(gdf, fields): # noqa: E501 # pylint: disable=W0613,R0912 :param gdf: DataFrame of results from query :type gdf: gpd.GeoDataFrame :param fields: List of requested columns - :type fields: dict + :type fields: str :rtype: dict, integer """ @@ -123,7 +317,7 @@ def format_csv(gdf, fields): # noqa: E501 # pylint: disable=W0613 :param gdf: DataFrame of results from query :type gdf: gpd.GeoDataFrame :param fields: List of requested columns - :type fields: dict + :type fields: str :rtype: dict, integer """ @@ -156,103 +350,45 @@ def add_units(gdf, columns): return columns + unit_columns -def validate_parameters(feature, feature_id, start_time, end_time, output, fields): - """ - Determine if all parameters are present and in the correct format. Return 400 - Bad Request if any errors are found alongside 0 hits. +def get_response(results, hits, elapsed, return_type, output): + """Create and return HTTP response based on results. - :param feature: Data requested for Reach or Node or Lake - :type feature: str - :param feature_id: ID of the feature to retrieve - :type feature_id: str - :param start_time: Start time of the timeseries - :type start_time: str - :param end_time: End time of the timeseries - :type end_time: str - :param output: Format of the data returned + :param results: Dictionary of SWOT timeseries results + :type results: dict + :param hits: Number of results returned from query + :type hits: int + :param elapsed: Number of seconds it took to query for results + :type elapsed: float + :param return_type: Accept request header + :type return_type: str + :param output: Output to return in request :type output: str - :param fields: List of requested columns - :type fields: dict - - :rtype: dict, integer - """ - - data = {'http_code': '200 OK'} - if feature not in ('Node', 'Reach'): - data['http_code'] = '400 Bad Request' - data['error_message'] = f'400: feature parameter should be Reach or Node, not: {feature}' - - elif not feature_id.isdigit(): - data['http_code'] = '400 Bad Request' - data['error_message'] = f'400: feature_id cannot contain letters: {feature_id}' - - elif not is_date_valid(start_time) or not is_date_valid(end_time): - data['http_code'] = '400 Bad Request' - data['error_message'] = '400: start_time and end_time parameters must conform to format: YYYY-MM-DDTHH:MM:SSZ or YYYY-MM-DDTHH:MM:SS-00:00' - - elif output not in ('csv', 'geojson'): - data['http_code'] = '400 Bad Request' - data['error_message'] = f'400: output parameter should be csv or geojson, not: {output}' - - elif not is_fields_valid(feature, fields): - data['http_code'] = '400 Bad Request' - data['error_message'] = '400: fields parameter should contain valid SWOT fields' - - return data, 0 - - -def is_date_valid(query_date): - """ - Check if the query date conforms to the correct format. - - :param start_time: Start or end time of the timeseries - :type start_time: str - - :rtype: bool - """ - - try: - datetime.datetime.strptime(query_date, "%Y-%m-%dT%H:%M:%S%z") - return True - except ValueError: - return False - -def is_fields_valid(feature, fields): + rtype: dict """ - Check if fields are present in either the reach or node list of columns - :param fields: List of requested columns - :type fields: dict + if results['http_code'] == '200 OK': - :rtype: bool - """ + if return_type in ('text/csv', 'application/geo+json'): + data = results['response'] + + else: # 'application/json' + data = { + 'status': results['http_code'], + 'time': elapsed, + 'hits': hits, + 'results': { + 'csv': "", + 'geojson': {} + } + } + data['results'][output] = results['response'] - fields = fields.split(',') - if feature == 'Reach': - columns = constants.REACH_ALL_COLUMNS - elif feature == 'Node': - columns = constants.NODE_ALL_COLUMNS else: - columns = [] - return all(field in columns for field in fields) - - -def sanitize_time(start_time, end_time): - """ - Return formatted string to handle cases where request includes non-padded numbers - - :param start_time: Start time of the timeseries - :type start_time: str - :param end_time: End time of the timeseries - :type end_time: str - - :rtype: str, str - """ + logging.error(results) + raise RequestError(results['error_message']) - start_time = datetime.datetime.strptime(start_time, "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%dT%H:%M:%S%z") - end_time = datetime.datetime.strptime(end_time, "%Y-%m-%dT%H:%M:%S%z").strftime("%Y-%m-%dT%H:%M:%S%z") - return start_time, end_time + return data def lambda_handler(event, context): # noqa: E501 # pylint: disable=W0613 @@ -266,42 +402,36 @@ def lambda_handler(event, context): # noqa: E501 # pylint: disable=W0613 logging.info('request: %s', json.dumps(event['body'])) try: - if event['body'] == {} and 'Elastic-Heartbeat' in event['headers']['User-Agent']: + headers = get_request_headers(event) + # The cloud metrics Elastic Heartbeat is a Health check that occurs + # every 15 seconds. The following statement checks if the current request is an Elastic Heartbeat + # and if it is, simply return success immediately instead of further processing the request + # More background: https://github.com/podaac/hydrocron/issues/89 + if event['body'] == {} and 'Elastic-Heartbeat' in headers['user_agent']: return {} - logging.info('user_ip: %s', event["headers"]["X-Forwarded-For"].split(",")[0]) - except KeyError as e: - logging.error('Error encountered with headers: %s', e) - raise RequestError(f'400: Issue encountered with request header: {e}') from e - - results = {'http_code': '200 OK'} - try: - feature = event['body']['feature'] - feature_id = event['body']['feature_id'] - start_time = event['body']['start_time'] - end_time = event['body']['end_time'] - output = event['body']['output'] - fields = event['body']['fields'] - results, hits = validate_parameters(feature, feature_id, start_time, end_time, output, fields) - except KeyError as e: - missing_parameter = str(e).rsplit(' ', maxsplit=1)[-1] - results['http_code'] = '400 Bad Request' - results['error_message'] = f'400: This required parameter is missing: {missing_parameter}' - hits = 0 - - if results['http_code'] == '200 OK': - start_time, end_time = sanitize_time(start_time, end_time) - results, hits = timeseries_get(feature, feature_id, start_time, end_time, output, fields) + logging.info('user_ip: %s', headers['user_ip']) + parameters = get_request_parameters(event) + return_type, output = get_return_type(headers['accept'], parameters['output']) + except RequestError as e: + raise e + + results, hits = timeseries_get( + parameters['feature'], + parameters['feature_id'], + parameters['start_time'], + parameters['end_time'], + output, + parameters['fields'] + ) end = time.time() elapsed = round((end - start) * 1000, 3) - data = {'status': results['http_code'], 'time': elapsed, 'hits': hits, 'results': {'csv': "", 'geojson': {}}} - if results['http_code'] == '200 OK': - data['results'][event['body']['output']] = results['response'] - logging.info('response: %s', json.dumps(data)) - logging.info('response_size: %s', str(sys.getsizeof(data))) - else: - logging.error(results) - raise RequestError(results['error_message']) + try: + data = get_response(results, hits, elapsed, return_type, output) + except RequestError as e: + raise e + logging.info('response: %s', json.dumps(data)) + logging.info('response_size: %s', str(sys.getsizeof(data))) return data diff --git a/hydrocron/api/swagger/swagger.yaml b/hydrocron/api/swagger/swagger.yaml deleted file mode 100644 index 34624d01..00000000 --- a/hydrocron/api/swagger/swagger.yaml +++ /dev/null @@ -1,111 +0,0 @@ -openapi: 3.0.0 -info: - title: "Get time series data from SWOT observations for reaches, nodes, and/or lakes" - description: "Get time series data from SWOT observations for reaches, nodes, and/or\ - \ lakes" - version: 1.0.0 -servers: -- url: https://virtserver.swaggerhub.com/hydrocron/HydroAPI/1.0.0 - description: "Get time series data from SWOT observations for reaches, nodes, and/or\ - \ lakes" -paths: - /timeseries: - get: - summary: "Get Timeseries for a particular Reach, Node, or LakeID" - description: "Get Timeseries for a particular Reach, Node, or LakeID" - operationId: timeseries_get - parameters: - - name: feature - in: query - description: Data requested for Reach or Node or Lake - required: false - style: form - explode: true - schema: - type: string - enum: [ "Reach", "Lake", "Node"] - example: Reach - - name: feature_id - in: query - description: ID of the feature to retrieve in format CBBTTTSNNNNNN (i.e. 74297700000000) - required: true - style: form - explode: true - schema: - type: string - example: 71224100223 - - name: start_time - in: query - description: Start time of the timeseries - required: true - style: form - explode: true - schema: - type: string - format: date-time - example: 2022-08-04T00:00:00Z - - name: end_time - in: query - description: End time of the timeseries - required: true - style: form - explode: true - schema: - type: string - format: date-time - example: 2022-08-23T00:00:00Z - - name: output - in: query - description: Format of the data returned - required: false - style: form - explode: true - schema: - type: string - enum: [ "csv", "geojson"] - default: geojson - example: geojson - - name: fields - in: query - description: Format of the data returned - required: false - style: form - explode: true - schema: - type: string - default: feature_id, time_str, wse, geometry - example: feature_id, time_str, wse, geometry - responses: - "200": - description: OK - content: - text/csv: - schema: - type: array - items: - type: string - "400": - description: "400 error. The specified URL is invalid (does not exist)." - content: - text/csv: - schema: - type: array - items: - type: string - "404": - description: "404 error. An entry with the specified region was not found." - content: - text/csv: - schema: - type: array - items: - type: string - "413": - description: "413 error. Your query has returned is too large." - content: - text/csv: - schema: - type: array - items: - type: string - x-openapi-router-controller: hydrocron.api.controllers.timeseries diff --git a/poetry.lock b/poetry.lock index 01353255..f6ca35e2 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,15 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "accept-types" +version = "0.4.1" +description = "Determine the best content to send in an HTTP response" +optional = false +python-versions = "*" +files = [ + {file = "accept-types-0.4.1.tar.gz", hash = "sha256:fb27099716d8f0360408c8ca86d69dbfed44455834b70d1506250abe521b535a"}, + {file = "accept_types-0.4.1-py3-none-any.whl", hash = "sha256:c87feccdffb66b02f9343ff387d7fd5c451ccb2e1221fbd37ea0cedef5cf290f"}, +] [[package]] name = "accessible-pygments" @@ -4673,4 +4684,4 @@ testing = ["big-O", "jaraco.functools", "jaraco.itertools", "more-itertools", "p [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "45c3fdb2de7f8e85d84f667c172bbc37470624638b57c720a176b1fa5896465c" +content-hash = "0ebb071ac4ce0ec1526c8dc0eb7b1bc30c3a7ea2aa6399d1d3928d45f6f49e7a" diff --git a/pyproject.toml b/pyproject.toml index 2e952e22..0833bf0a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ shapely = "^2.0.1" cryptography = "42.0.0" python-dotenv = "^1.0.0" geojson = "^3.1.0" +accept-types = "^0.4.1" [tool.poetry.group.dev.dependencies] pytest = "^7.4.0" diff --git a/terraform/api-specification-templates/hydrocron_aws_api.yml b/terraform/api-specification-templates/hydrocron_aws_api.yml index ad70ab76..e19ef22f 100644 --- a/terraform/api-specification-templates/hydrocron_aws_api.yml +++ b/terraform/api-specification-templates/hydrocron_aws_api.yml @@ -83,10 +83,13 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/geojson' + $ref: '#/components/schemas/json' text/csv: schema: $ref: '#/components/schemas/csv' + application/geo+json: + schema: + $ref: '#/components/schemas/geojson' "400": description: "400 error. The specified URL is invalid (does not exist)." content: @@ -111,6 +114,14 @@ paths: type: array items: type: string + "415": + description: "415 error. Your request includes an unsupported media type." + content: + text/csv: + schema: + type: array + items: + type: string x-openapi-router-controller: hydrocron_api.controllers.timeseries x-amazon-apigateway-integration: @@ -127,6 +138,18 @@ paths: #set($context.responseOverride.status = 206) #end $input.json('$') + application/geo+json: | + #set($inputRoot = $input.path('$')) + #if($inputRoot.toString().contains('206 PARTIAL CONTENT')) + #set($context.responseOverride.status = 206) + #end + $input.json('$') + text/csv: | + #set($inputRoot = $input.path('$')) + #if($inputRoot.toString().contains('206 PARTIAL CONTENT')) + #set($context.responseOverride.status = 206) + #end + $input.body ^400.*: statusCode: "400" responseTemplates: @@ -148,6 +171,13 @@ paths: { "error" : "$input.path('$.errorMessage')" } + ^415.*: + statusCode: "415" + responseTemplates: + application/json: |- + { + "error" : "$input.path('$.errorMessage')" + } ^[^1-5].*: statusCode: "500" responseTemplates: @@ -212,6 +242,8 @@ paths: method.response.header.Access-Control-Allow-Origin: "'*'" components: schemas: + json: + type: object geojson: type: object csv: diff --git a/tests/test_api.py b/tests/test_api.py index 54bfa0da..8bfb61ec 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -1,6 +1,7 @@ """ Tests for API queries """ +import csv import os.path import pathlib @@ -12,7 +13,7 @@ from numpy.testing import assert_almost_equal -def test_timeseries_lambda_handler_geojson(hydrocron_api): +def test_timeseries_lambda_handler_json(hydrocron_api): """ Test the lambda handler for the timeseries endpoint Parameters @@ -38,200 +39,12 @@ def test_timeseries_lambda_handler_geojson(hydrocron_api): context = "_" result = hydrocron.api.controllers.timeseries.lambda_handler(event, context) + test_data = (pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + .joinpath('test_data').joinpath('api_query_results_geojson.json')) + with open(test_data) as jf: + expected = json.load(jf) assert result['status'] == '200 OK' and \ - result['results']['geojson'] == {'type': 'FeatureCollection', 'features': [ - {'id': '0', 'properties': {'reach_id': '71224100223', 'time_str': '2023-06-10T19:39:43Z', - 'wse': '286.2983', 'sword_version': '15', - 'collection_shortname': 'SWOT_L2_HR_RiverSP_2.0', 'crid': 'PIA1', 'wse_units': 'm'}, - 'geometry': {'coordinates': - [[-95.564991, 50.223686], [-95.564559, 50.223479], - [-95.564133, 50.223381], - [-95.563713, 50.22339], [-95.563296, 50.223453], - [-95.562884, 50.223624], - [-95.562473, 50.223795], [-95.562062, 50.223966], - [-95.56165, 50.224137], - [-95.561242, 50.224362], [-95.560917, 50.224585], - [-95.560595, 50.224862], - [-95.560271, 50.225085], [-95.559946, 50.225308], - [-95.559946, 50.225308], - [-95.559213, 50.225756], [-95.558804, 50.225981], - [-95.558567, 50.226256], - [-95.558413, 50.226529], [-95.558343, 50.226801], - [-95.558274, 50.227072], - [-95.558288, 50.227342], [-95.558303, 50.227611], - [-95.558317, 50.227881], - [-95.558416, 50.228148], [-95.558514, 50.228416], - [-95.558697, 50.228682], - [-95.558795, 50.22895], [-95.558978, 50.229216], - [-95.559076, 50.229483], - [-95.559259, 50.229749], [-95.559357, 50.230017], - [-95.559455, 50.230284], - [-95.55947, 50.230554], [-95.559484, 50.230823], - [-95.559583, 50.231091], - [-95.559765, 50.231357], [-95.559864, 50.231625], - [-95.559878, 50.231894], - [-95.559809, 50.232166], [-95.559571, 50.232441], - [-95.559165, 50.23272], - [-95.558757, 50.232944], [-95.558348, 50.233169], - [-95.557939, 50.233394], - [-95.55753, 50.233619], [-95.557206, 50.233842], - [-95.556884, 50.234119], - [-95.556562, 50.234396], [-95.556241, 50.234673], - [-95.556003, 50.234948], - [-95.555681, 50.235225], [-95.555443, 50.2355], - [-95.555206, 50.235775], - [-95.555136, 50.236047], [-95.555066, 50.236318], - [-95.555081, 50.236588], - [-95.555011, 50.236859], [-95.554941, 50.23713], - [-95.554701, 50.237351], - [-95.554376, 50.237575], [-95.554052, 50.237798], - [-95.553727, 50.238021], - [-95.553727, 50.238021], [-95.55308, 50.238521], - [-95.552843, 50.238796], - [-95.552521, 50.239073], [-95.552367, 50.239346], - [-95.552297, 50.239617], - [-95.552312, 50.239887], [-95.552326, 50.240156], - [-95.552425, 50.240424], - [-95.552439, 50.240693], [-95.552453, 50.240963], - [-95.552468, 50.241233], - [-95.552482, 50.241502], [-95.552497, 50.241772], - [-95.552511, 50.242041], - [-95.552525, 50.242311], [-95.552456, 50.242582], - [-95.552299, 50.242801], - [-95.552056, 50.242969], [-95.551728, 50.243138], - [-95.551314, 50.243255], - [-95.550899, 50.243372], [-95.550487, 50.243543], - [-95.550076, 50.243714], - [-95.549661, 50.243831], [-95.549249, 50.244002], - [-95.548838, 50.244173], - [-95.548423, 50.24429], [-95.548011, 50.244461], - [-95.547603, 50.244686], - [-95.547278, 50.244909], [-95.546953, 50.245132], - [-95.546715, 50.245407], - [-95.546561, 50.24568], [-95.546492, 50.245951], - [-95.546422, 50.246223], - [-95.546436, 50.246492], [-95.546367, 50.246764], - [-95.546297, 50.247035], - [-95.546143, 50.247308], [-95.54599, 50.247582], - [-95.545752, 50.247857], - [-95.545598, 50.24813], [-95.545444, 50.248403], - [-95.545374, 50.248674], - [-95.545305, 50.248946], [-95.545319, 50.249215], - [-95.545333, 50.249485], - [-95.545348, 50.249754], [-95.545446, 50.250022], - [-95.545545, 50.25029], - [-95.545727, 50.250556], [-95.54591, 50.250822], - [-95.546176, 50.251086], - [-95.546359, 50.251351], [-95.546625, 50.251615], - [-95.546892, 50.251879], - [-95.547075, 50.252145], [-95.547257, 50.252411], - [-95.54744, 50.252677], - [-95.547538, 50.252945], [-95.547553, 50.253214], - [-95.547651, 50.253482], - [-95.547581, 50.253753], [-95.547512, 50.254025], - [-95.547442, 50.254296], - [-95.547372, 50.254568], [-95.547303, 50.254839], - [-95.547317, 50.255108], - [-95.547247, 50.25538], [-95.547177, 50.255651], - [-95.547192, 50.255921], - [-95.547206, 50.25619], [-95.547221, 50.25646], - [-95.547319, 50.256728], - [-95.547418, 50.256995], [-95.547432, 50.257265], - [-95.547446, 50.257534], - [-95.547461, 50.257804], [-95.547475, 50.258073], - [-95.547489, 50.258343], - [-95.547504, 50.258613], [-95.547518, 50.258882], - [-95.547449, 50.259153], - [-95.547379, 50.259425], [-95.547309, 50.259696], - [-95.547239, 50.259968], - [-95.547086, 50.260241], [-95.547016, 50.260512], - [-95.546946, 50.260784], - [-95.546876, 50.261055], [-95.546807, 50.261326], - [-95.546737, 50.261598], - [-95.546583, 50.261871], [-95.546345, 50.262146], - [-95.54602, 50.262369], - [-95.545611, 50.262594], [-95.5452, 50.262765], - [-95.544788, 50.262936], - [-95.544373, 50.263053], [-95.543961, 50.263224], - [-95.543546, 50.263341], - [-95.543135, 50.263512], [-95.542723, 50.263683], - [-95.542314, 50.263907], - [-95.541989, 50.26413], [-95.541667, 50.264407], - [-95.541345, 50.264684], - [-95.541107, 50.264959], [-95.540869, 50.265234], - [-95.540631, 50.265509], - [-95.540393, 50.265785], [-95.540155, 50.26606], - [-95.539917, 50.266335], - [-95.539679, 50.26661], [-95.539441, 50.266885], - [-95.539203, 50.26716], - [-95.538962, 50.267381], [-95.538634, 50.26755], - [-95.538304, 50.267665], - [-95.537889, 50.267782], [-95.537471, 50.267845], - [-95.537056, 50.267962], - [-95.536642, 50.268079], [-95.536227, 50.268196], - [-95.535809, 50.268259], - [-95.535391, 50.268323], [-95.534974, 50.268386], - [-95.534556, 50.268449], - [-95.534138, 50.268512], [-95.533718, 50.268521], - [-95.5333, 50.268584], - [-95.532882, 50.268647], [-95.532552, 50.268762], - [-95.532224, 50.268931], - [-95.531986, 50.269206], [-95.531748, 50.269481], - [-95.531594, 50.269755], - [-95.531356, 50.27003], [-95.531202, 50.270303], - [-95.531048, 50.270576], - [-95.530978, 50.270847], [-95.530908, 50.271119], - [-95.530923, 50.271388], - [-95.530937, 50.271658], [-95.530951, 50.271927], - [-95.53105, 50.272195], - [-95.531148, 50.272463], [-95.531331, 50.272729], - [-95.531513, 50.272995], - [-95.531696, 50.273261], [-95.531878, 50.273526], - [-95.532145, 50.27379], - [-95.532327, 50.274056], [-95.532594, 50.27432], - [-95.532861, 50.274584], - [-95.533043, 50.27485], [-95.533142, 50.275118], - [-95.53324, 50.275386], - [-95.533339, 50.275653], [-95.533437, 50.275921], - [-95.533536, 50.276189], - [-95.533634, 50.276457], [-95.533732, 50.276724], - [-95.533747, 50.276994], - [-95.533761, 50.277263], [-95.533859, 50.277531], - [-95.533958, 50.277799], - [-95.53414, 50.278065], [-95.534323, 50.278331], - [-95.534506, 50.278596], - [-95.534688, 50.278862], [-95.534871, 50.279128], - [-95.534969, 50.279396], - [-95.535152, 50.279662], [-95.535334, 50.279928], - [-95.535433, 50.280195], - [-95.535615, 50.280461], [-95.535798, 50.280727], - [-95.535812, 50.280997], - [-95.535743, 50.281268], [-95.535589, 50.281541], - [-95.535266, 50.281818], - [-95.53486, 50.282097], [-95.534454, 50.282376], - [-95.534132, 50.282652], - [-95.533893, 50.282927], [-95.533824, 50.283199], - [-95.533838, 50.283468], - [-95.534021, 50.283734], [-95.534203, 50.284], - [-95.53447, 50.284264], - [-95.534652, 50.28453], [-95.534835, 50.284796], - [-95.535018, 50.285062], - [-95.5352, 50.285328], [-95.535383, 50.285594], - [-95.535565, 50.285859], - [-95.535832, 50.286123], [-95.536099, 50.286387], - [-95.53645, 50.28665], - [-95.536801, 50.286912], [-95.537152, 50.287174], - [-95.537418, 50.287438], - [-95.537601, 50.287704], [-95.5377, 50.287972], - [-95.537798, 50.288239], - [-95.537897, 50.288507], [-95.537995, 50.288775], - [-95.538093, 50.289043], - [-95.538192, 50.28931], [-95.538206, 50.28958], - [-95.538221, 50.289849], - [-95.538235, 50.290119], [-95.538334, 50.290387], - [-95.538432, 50.290654], - [-95.538531, 50.290922], [-95.538629, 50.29119]], - 'type': 'LineString'}, 'type': 'Feature'}]} + result['results']['geojson'] == expected def test_timeseries_lambda_handler_validate_geojson_reach(hydrocron_api): @@ -291,91 +104,14 @@ def test_timeseries_lambda_handler_csv(hydrocron_api): context = "_" result = hydrocron.api.controllers.timeseries.lambda_handler(event, context) assert result['status'] == '200 OK' - assert result['results']['csv'] == ( - 'reach_id,time_str,wse,sword_version,collection_shortname,crid,geometry,wse_units\n' \ - '71224100223,2023-06-10T19:39:43Z,286.2983,15,SWOT_L2_HR_RiverSP_2.0,PIA1,' \ - '"LINESTRING (-95.564991 50.223686, ' \ - '-95.564559 50.223479, -95.564133 50.223381, -95.563713 50.22339, -95.563296 ' \ - '50.223453, -95.562884 50.223624, -95.562473 50.223795, -95.562062 50.223966, ' \ - '-95.56165 50.224137, -95.561242 50.224362, -95.560917 50.224585, -95.560595 ' \ - '50.224862, -95.560271 50.225085, -95.559946 50.225308, -95.559946 50.225308, ' \ - '-95.559213 50.225756, -95.558804 50.225981, -95.558567 50.226256, -95.558413 ' \ - '50.226529, -95.558343 50.226801, -95.558274 50.227072, -95.558288 50.227342, ' \ - '-95.558303 50.227611, -95.558317 50.227881, -95.558416 50.228148, -95.558514 ' \ - '50.228416, -95.558697 50.228682, -95.558795 50.22895, -95.558978 50.229216, ' \ - '-95.559076 50.229483, -95.559259 50.229749, -95.559357 50.230017, -95.559455 ' \ - '50.230284, -95.55947 50.230554, -95.559484 50.230823, -95.559583 50.231091, ' \ - '-95.559765 50.231357, -95.559864 50.231625, -95.559878 50.231894, -95.559809 ' \ - '50.232166, -95.559571 50.232441, -95.559165 50.23272, -95.558757 50.232944, ' \ - '-95.558348 50.233169, -95.557939 50.233394, -95.55753 50.233619, -95.557206 ' \ - '50.233842, -95.556884 50.234119, -95.556562 50.234396, -95.556241 50.234673, ' \ - '-95.556003 50.234948, -95.555681 50.235225, -95.555443 50.2355, -95.555206 ' \ - '50.235775, -95.555136 50.236047, -95.555066 50.236318, -95.555081 50.236588, ' \ - '-95.555011 50.236859, -95.554941 50.23713, -95.554701 50.237351, -95.554376 ' \ - '50.237575, -95.554052 50.237798, -95.553727 50.238021, -95.553727 50.238021, ' \ - '-95.55308 50.238521, -95.552843 50.238796, -95.552521 50.239073, -95.552367 ' \ - '50.239346, -95.552297 50.239617, -95.552312 50.239887, -95.552326 50.240156, ' \ - '-95.552425 50.240424, -95.552439 50.240693, -95.552453 50.240963, -95.552468 ' \ - '50.241233, -95.552482 50.241502, -95.552497 50.241772, -95.552511 50.242041, ' \ - '-95.552525 50.242311, -95.552456 50.242582, -95.552299 50.242801, -95.552056 ' \ - '50.242969, -95.551728 50.243138, -95.551314 50.243255, -95.550899 50.243372, ' \ - '-95.550487 50.243543, -95.550076 50.243714, -95.549661 50.243831, -95.549249 ' \ - '50.244002, -95.548838 50.244173, -95.548423 50.24429, -95.548011 50.244461, ' \ - '-95.547603 50.244686, -95.547278 50.244909, -95.546953 50.245132, -95.546715 ' \ - '50.245407, -95.546561 50.24568, -95.546492 50.245951, -95.546422 50.246223, ' \ - '-95.546436 50.246492, -95.546367 50.246764, -95.546297 50.247035, -95.546143 ' \ - '50.247308, -95.54599 50.247582, -95.545752 50.247857, -95.545598 50.24813, ' \ - '-95.545444 50.248403, -95.545374 50.248674, -95.545305 50.248946, -95.545319 ' \ - '50.249215, -95.545333 50.249485, -95.545348 50.249754, -95.545446 50.250022, ' \ - '-95.545545 50.25029, -95.545727 50.250556, -95.54591 50.250822, -95.546176 ' \ - '50.251086, -95.546359 50.251351, -95.546625 50.251615, -95.546892 50.251879, ' \ - '-95.547075 50.252145, -95.547257 50.252411, -95.54744 50.252677, -95.547538 ' \ - '50.252945, -95.547553 50.253214, -95.547651 50.253482, -95.547581 50.253753, ' \ - '-95.547512 50.254025, -95.547442 50.254296, -95.547372 50.254568, -95.547303 ' \ - '50.254839, -95.547317 50.255108, -95.547247 50.25538, -95.547177 50.255651, ' \ - '-95.547192 50.255921, -95.547206 50.25619, -95.547221 50.25646, -95.547319 ' \ - '50.256728, -95.547418 50.256995, -95.547432 50.257265, -95.547446 50.257534, ' \ - '-95.547461 50.257804, -95.547475 50.258073, -95.547489 50.258343, -95.547504 ' \ - '50.258613, -95.547518 50.258882, -95.547449 50.259153, -95.547379 50.259425, ' \ - '-95.547309 50.259696, -95.547239 50.259968, -95.547086 50.260241, -95.547016 ' \ - '50.260512, -95.546946 50.260784, -95.546876 50.261055, -95.546807 50.261326, ' \ - '-95.546737 50.261598, -95.546583 50.261871, -95.546345 50.262146, -95.54602 ' \ - '50.262369, -95.545611 50.262594, -95.5452 50.262765, -95.544788 50.262936, ' \ - '-95.544373 50.263053, -95.543961 50.263224, -95.543546 50.263341, -95.543135 ' \ - '50.263512, -95.542723 50.263683, -95.542314 50.263907, -95.541989 50.26413, ' \ - '-95.541667 50.264407, -95.541345 50.264684, -95.541107 50.264959, -95.540869 ' \ - '50.265234, -95.540631 50.265509, -95.540393 50.265785, -95.540155 50.26606, ' \ - '-95.539917 50.266335, -95.539679 50.26661, -95.539441 50.266885, -95.539203 ' \ - '50.26716, -95.538962 50.267381, -95.538634 50.26755, -95.538304 50.267665, ' \ - '-95.537889 50.267782, -95.537471 50.267845, -95.537056 50.267962, -95.536642 ' \ - '50.268079, -95.536227 50.268196, -95.535809 50.268259, -95.535391 50.268323, ' \ - '-95.534974 50.268386, -95.534556 50.268449, -95.534138 50.268512, -95.533718 ' \ - '50.268521, -95.5333 50.268584, -95.532882 50.268647, -95.532552 50.268762, ' \ - '-95.532224 50.268931, -95.531986 50.269206, -95.531748 50.269481, -95.531594 ' \ - '50.269755, -95.531356 50.27003, -95.531202 50.270303, -95.531048 50.270576, ' \ - '-95.530978 50.270847, -95.530908 50.271119, -95.530923 50.271388, -95.530937 ' \ - '50.271658, -95.530951 50.271927, -95.53105 50.272195, -95.531148 50.272463, ' \ - '-95.531331 50.272729, -95.531513 50.272995, -95.531696 50.273261, -95.531878 ' \ - '50.273526, -95.532145 50.27379, -95.532327 50.274056, -95.532594 50.27432, ' \ - '-95.532861 50.274584, -95.533043 50.27485, -95.533142 50.275118, -95.53324 ' \ - '50.275386, -95.533339 50.275653, -95.533437 50.275921, -95.533536 50.276189, ' \ - '-95.533634 50.276457, -95.533732 50.276724, -95.533747 50.276994, -95.533761 ' \ - '50.277263, -95.533859 50.277531, -95.533958 50.277799, -95.53414 50.278065, ' \ - '-95.534323 50.278331, -95.534506 50.278596, -95.534688 50.278862, -95.534871 ' \ - '50.279128, -95.534969 50.279396, -95.535152 50.279662, -95.535334 50.279928, ' \ - '-95.535433 50.280195, -95.535615 50.280461, -95.535798 50.280727, -95.535812 ' \ - '50.280997, -95.535743 50.281268, -95.535589 50.281541, -95.535266 50.281818, ' \ - '-95.53486 50.282097, -95.534454 50.282376, -95.534132 50.282652, -95.533893 ' \ - '50.282927, -95.533824 50.283199, -95.533838 50.283468, -95.534021 50.283734, ' \ - '-95.534203 50.284, -95.53447 50.284264, -95.534652 50.28453, -95.534835 ' \ - '50.284796, -95.535018 50.285062, -95.5352 50.285328, -95.535383 50.285594, ' \ - '-95.535565 50.285859, -95.535832 50.286123, -95.536099 50.286387, -95.53645 ' \ - '50.28665, -95.536801 50.286912, -95.537152 50.287174, -95.537418 50.287438, ' \ - '-95.537601 50.287704, -95.5377 50.287972, -95.537798 50.288239, -95.537897 ' \ - '50.288507, -95.537995 50.288775, -95.538093 50.289043, -95.538192 50.28931, ' \ - '-95.538206 50.28958, -95.538221 50.289849, -95.538235 50.290119, -95.538334 ' \ - '50.290387, -95.538432 50.290654, -95.538531 50.290922, -95.538629 ' \ - '50.29119)",m\n') + test_data = (pathlib.Path(os.path.dirname(os.path.realpath(__file__))).joinpath('test_data').joinpath('api_query_results_csv.csv')) + with open(test_data) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',', quotechar=None) + row_str = "" + for row in csv_reader: + row_str += f"{','.join(row)}\n" + + assert result['results']['csv'] == row_str def test_timeseries_convert_to_df_node(hydrocron_api): @@ -706,3 +442,231 @@ def test_timeseries_lambda_handler_missing_header(hydrocron_api): with pytest.raises(hydrocron.api.controllers.timeseries.RequestError) as e: hydrocron.api.controllers.timeseries.lambda_handler(event, context) assert "400: Issue encountered with request headers" in str(e.value) + + +def test_timeseries_lambda_handler_geojson_accept(hydrocron_api): + """ + Test the lambda handler for the timeseries endpoint + Parameters + ---------- + hydrocron_api: Fixture ensuring the database is configured for the api + """ + import hydrocron.api.controllers.timeseries + + event = { + "body": { + "feature": "Reach", + "feature_id": "71224100223", + "start_time": "2023-06-04T00:00:00Z", + "end_time": "2023-06-23T00:00:00Z", + "fields": "reach_id,time_str,wse,sword_version,collection_shortname,crid" + }, + "headers": { + "User-Agent": "curl/8.4.0", + "X-Forwarded-For": "123.456.789.000", + "Accept": "application/geo+json" + } + } + + context = "_" + result = hydrocron.api.controllers.timeseries.lambda_handler(event, context) + test_data = (pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + .joinpath('test_data').joinpath('api_query_results_geojson.json')) + with open(test_data) as jf: + expected = json.load(jf) + assert result == expected + + +def test_timeseries_lambda_handler_csv_accept(hydrocron_api): + """ + Test the lambda handler for the timeseries endpoint + Parameters + ---------- + hydrocron_api: Fixture ensuring the database is configured for the api + """ + + import hydrocron.api.controllers.timeseries + + event = { + "body": { + "feature": "Reach", + "feature_id": "71224100223", + "start_time": "2023-06-04T00:00:00Z", + "end_time": "2023-06-23T00:00:00Z", + "fields": "reach_id,time_str,wse,sword_version,collection_shortname,crid,geometry" + }, + "headers": { + "User-Agent": "curl/8.4.0", + "X-Forwarded-For": "123.456.789.000", + "Accept": "text/csv" + } + } + + context = "_" + result = hydrocron.api.controllers.timeseries.lambda_handler(event, context) + + test_data = (pathlib.Path(os.path.dirname(os.path.realpath(__file__))).joinpath('test_data').joinpath('api_query_results_csv.csv')) + with open(test_data) as csv_file: + csv_reader = csv.reader(csv_file, delimiter=',', quotechar=None) + row_str = "" + for row in csv_reader: + row_str += f"{','.join(row)}\n" + + assert result == row_str + + +def test_timeseries_lambda_handler_geojson_accept_output(hydrocron_api): + """ + Test the lambda handler for the timeseries endpoint + Parameters + ---------- + hydrocron_api: Fixture ensuring the database is configured for the api + """ + import hydrocron.api.controllers.timeseries + + event = { + "body": { + "feature": "Reach", + "feature_id": "71224100223", + "start_time": "2023-06-04T00:00:00Z", + "end_time": "2023-06-23T00:00:00Z", + "output": "geojson", + "fields": "reach_id,time_str,wse,sword_version,collection_shortname,crid" + }, + "headers": { + "User-Agent": "curl/8.4.0", + "X-Forwarded-For": "123.456.789.000", + "Accept": "application/geo+json" + } + } + + context = "_" + with pytest.raises(hydrocron.api.controllers.timeseries.RequestError) as e: + hydrocron.api.controllers.timeseries.lambda_handler(event, context) + assert "400: Invalid combination of Accept header and output request parameter" in str(e.value) + + +def test_timeseries_lambda_handler_json_no_output(hydrocron_api): + """ + Test the lambda handler for the timeseries endpoint + Parameters + ---------- + hydrocron_api: Fixture ensuring the database is configured for the api + """ + import hydrocron.api.controllers.timeseries + + event = { + "body": { + "feature": "Reach", + "feature_id": "71224100223", + "start_time": "2023-06-04T00:00:00Z", + "end_time": "2023-06-23T00:00:00Z", + "fields": "reach_id,time_str,wse,sword_version,collection_shortname,crid" + }, + "headers": { + "User-Agent": "curl/8.4.0", + "X-Forwarded-For": "123.456.789.000" + } + } + + context = "_" + result = hydrocron.api.controllers.timeseries.lambda_handler(event, context) + test_data = (pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + .joinpath('test_data').joinpath('api_query_results_geojson.json')) + with open(test_data) as jf: + expected = json.load(jf) + assert result['status'] == '200 OK' and \ + result['results']['geojson'] == expected + +def test_timeseries_lambda_handler_json_multi_accept(hydrocron_api): + """ + Test the lambda handler for the timeseries endpoint + Parameters + ---------- + hydrocron_api: Fixture ensuring the database is configured for the api + """ + import hydrocron.api.controllers.timeseries + + event = { + "body": { + "feature": "Reach", + "feature_id": "71224100223", + "start_time": "2023-06-04T00:00:00Z", + "end_time": "2023-06-23T00:00:00Z", + "fields": "reach_id,time_str,wse,sword_version,collection_shortname,crid" + }, + "headers": { + "User-Agent": "curl/8.4.0", + "X-Forwarded-For": "123.456.789.000", + "Accept": "image/webp,image/*,*/*;q=0.8" + } + } + + context = "_" + result = hydrocron.api.controllers.timeseries.lambda_handler(event, context) + test_data = (pathlib.Path(os.path.dirname(os.path.realpath(__file__))) + .joinpath('test_data').joinpath('api_query_results_geojson.json')) + with open(test_data) as jf: + expected = json.load(jf) + assert result['status'] == '200 OK' and \ + result['results']['geojson'] == expected + + +def test_timeseries_lambda_handler_unsupported(hydrocron_api): + """ + Test the lambda handler for the timeseries endpoint + Parameters + ---------- + hydrocron_api: Fixture ensuring the database is configured for the api + """ + import hydrocron.api.controllers.timeseries + + event = { + "body": { + "feature": "Reach", + "feature_id": "71224100223", + "start_time": "2023-06-04T00:00:00Z", + "end_time": "2023-06-23T00:00:00Z", + "fields": "reach_id,time_str,wse,sword_version,collection_shortname,crid" + }, + "headers": { + "User-Agent": "curl/8.4.0", + "X-Forwarded-For": "123.456.789.000", + "Accept": "image/jpg" + } + } + + context = "_" + with pytest.raises(hydrocron.api.controllers.timeseries.RequestError) as e: + hydrocron.api.controllers.timeseries.lambda_handler(event, context) + assert "415: Unsupported media type in Accept request header: image/jpg." in str(e.value) + + +def test_timeseries_lambda_handler_reachid_not_found(hydrocron_api): + """ + Test the lambda handler for the timeseries endpoint + Parameters + ---------- + hydrocron_api: Fixture ensuring the database is configured for the api + """ + import hydrocron.api.controllers.timeseries + + event = { + "body": { + "feature": "Reach", + "feature_id": "71224100228", + "start_time": "2023-06-04T00:00:00Z", + "end_time": "2023-06-23T00:00:00Z", + "fields": "reach_id,time_str,wse,sword_version,collection_shortname,crid" + }, + "headers": { + "User-Agent": "curl/8.4.0", + "X-Forwarded-For": "123.456.789.000", + "Accept": "image/jpg" + } + } + + context = "_" + with pytest.raises(hydrocron.api.controllers.timeseries.RequestError) as e: + hydrocron.api.controllers.timeseries.lambda_handler(event, context) + assert "400: Results with the specified Feature ID 71224100228 were not found" in str(e.value) diff --git a/tests/test_data/api_query_results_csv.csv b/tests/test_data/api_query_results_csv.csv new file mode 100644 index 00000000..ecda0f46 --- /dev/null +++ b/tests/test_data/api_query_results_csv.csv @@ -0,0 +1,2 @@ +reach_id,time_str,wse,sword_version,collection_shortname,crid,geometry,wse_units +71224100223,2023-06-10T19:39:43Z,286.2983,15,SWOT_L2_HR_RiverSP_2.0,PIA1,"LINESTRING (-95.564991 50.223686, -95.564559 50.223479, -95.564133 50.223381, -95.563713 50.22339, -95.563296 50.223453, -95.562884 50.223624, -95.562473 50.223795, -95.562062 50.223966, -95.56165 50.224137, -95.561242 50.224362, -95.560917 50.224585, -95.560595 50.224862, -95.560271 50.225085, -95.559946 50.225308, -95.559946 50.225308, -95.559213 50.225756, -95.558804 50.225981, -95.558567 50.226256, -95.558413 50.226529, -95.558343 50.226801, -95.558274 50.227072, -95.558288 50.227342, -95.558303 50.227611, -95.558317 50.227881, -95.558416 50.228148, -95.558514 50.228416, -95.558697 50.228682, -95.558795 50.22895, -95.558978 50.229216, -95.559076 50.229483, -95.559259 50.229749, -95.559357 50.230017, -95.559455 50.230284, -95.55947 50.230554, -95.559484 50.230823, -95.559583 50.231091, -95.559765 50.231357, -95.559864 50.231625, -95.559878 50.231894, -95.559809 50.232166, -95.559571 50.232441, -95.559165 50.23272, -95.558757 50.232944, -95.558348 50.233169, -95.557939 50.233394, -95.55753 50.233619, -95.557206 50.233842, -95.556884 50.234119, -95.556562 50.234396, -95.556241 50.234673, -95.556003 50.234948, -95.555681 50.235225, -95.555443 50.2355, -95.555206 50.235775, -95.555136 50.236047, -95.555066 50.236318, -95.555081 50.236588, -95.555011 50.236859, -95.554941 50.23713, -95.554701 50.237351, -95.554376 50.237575, -95.554052 50.237798, -95.553727 50.238021, -95.553727 50.238021, -95.55308 50.238521, -95.552843 50.238796, -95.552521 50.239073, -95.552367 50.239346, -95.552297 50.239617, -95.552312 50.239887, -95.552326 50.240156, -95.552425 50.240424, -95.552439 50.240693, -95.552453 50.240963, -95.552468 50.241233, -95.552482 50.241502, -95.552497 50.241772, -95.552511 50.242041, -95.552525 50.242311, -95.552456 50.242582, -95.552299 50.242801, -95.552056 50.242969, -95.551728 50.243138, -95.551314 50.243255, -95.550899 50.243372, -95.550487 50.243543, -95.550076 50.243714, -95.549661 50.243831, -95.549249 50.244002, -95.548838 50.244173, -95.548423 50.24429, -95.548011 50.244461, -95.547603 50.244686, -95.547278 50.244909, -95.546953 50.245132, -95.546715 50.245407, -95.546561 50.24568, -95.546492 50.245951, -95.546422 50.246223, -95.546436 50.246492, -95.546367 50.246764, -95.546297 50.247035, -95.546143 50.247308, -95.54599 50.247582, -95.545752 50.247857, -95.545598 50.24813, -95.545444 50.248403, -95.545374 50.248674, -95.545305 50.248946, -95.545319 50.249215, -95.545333 50.249485, -95.545348 50.249754, -95.545446 50.250022, -95.545545 50.25029, -95.545727 50.250556, -95.54591 50.250822, -95.546176 50.251086, -95.546359 50.251351, -95.546625 50.251615, -95.546892 50.251879, -95.547075 50.252145, -95.547257 50.252411, -95.54744 50.252677, -95.547538 50.252945, -95.547553 50.253214, -95.547651 50.253482, -95.547581 50.253753, -95.547512 50.254025, -95.547442 50.254296, -95.547372 50.254568, -95.547303 50.254839, -95.547317 50.255108, -95.547247 50.25538, -95.547177 50.255651, -95.547192 50.255921, -95.547206 50.25619, -95.547221 50.25646, -95.547319 50.256728, -95.547418 50.256995, -95.547432 50.257265, -95.547446 50.257534, -95.547461 50.257804, -95.547475 50.258073, -95.547489 50.258343, -95.547504 50.258613, -95.547518 50.258882, -95.547449 50.259153, -95.547379 50.259425, -95.547309 50.259696, -95.547239 50.259968, -95.547086 50.260241, -95.547016 50.260512, -95.546946 50.260784, -95.546876 50.261055, -95.546807 50.261326, -95.546737 50.261598, -95.546583 50.261871, -95.546345 50.262146, -95.54602 50.262369, -95.545611 50.262594, -95.5452 50.262765, -95.544788 50.262936, -95.544373 50.263053, -95.543961 50.263224, -95.543546 50.263341, -95.543135 50.263512, -95.542723 50.263683, -95.542314 50.263907, -95.541989 50.26413, -95.541667 50.264407, -95.541345 50.264684, -95.541107 50.264959, -95.540869 50.265234, -95.540631 50.265509, -95.540393 50.265785, -95.540155 50.26606, -95.539917 50.266335, -95.539679 50.26661, -95.539441 50.266885, -95.539203 50.26716, -95.538962 50.267381, -95.538634 50.26755, -95.538304 50.267665, -95.537889 50.267782, -95.537471 50.267845, -95.537056 50.267962, -95.536642 50.268079, -95.536227 50.268196, -95.535809 50.268259, -95.535391 50.268323, -95.534974 50.268386, -95.534556 50.268449, -95.534138 50.268512, -95.533718 50.268521, -95.5333 50.268584, -95.532882 50.268647, -95.532552 50.268762, -95.532224 50.268931, -95.531986 50.269206, -95.531748 50.269481, -95.531594 50.269755, -95.531356 50.27003, -95.531202 50.270303, -95.531048 50.270576, -95.530978 50.270847, -95.530908 50.271119, -95.530923 50.271388, -95.530937 50.271658, -95.530951 50.271927, -95.53105 50.272195, -95.531148 50.272463, -95.531331 50.272729, -95.531513 50.272995, -95.531696 50.273261, -95.531878 50.273526, -95.532145 50.27379, -95.532327 50.274056, -95.532594 50.27432, -95.532861 50.274584, -95.533043 50.27485, -95.533142 50.275118, -95.53324 50.275386, -95.533339 50.275653, -95.533437 50.275921, -95.533536 50.276189, -95.533634 50.276457, -95.533732 50.276724, -95.533747 50.276994, -95.533761 50.277263, -95.533859 50.277531, -95.533958 50.277799, -95.53414 50.278065, -95.534323 50.278331, -95.534506 50.278596, -95.534688 50.278862, -95.534871 50.279128, -95.534969 50.279396, -95.535152 50.279662, -95.535334 50.279928, -95.535433 50.280195, -95.535615 50.280461, -95.535798 50.280727, -95.535812 50.280997, -95.535743 50.281268, -95.535589 50.281541, -95.535266 50.281818, -95.53486 50.282097, -95.534454 50.282376, -95.534132 50.282652, -95.533893 50.282927, -95.533824 50.283199, -95.533838 50.283468, -95.534021 50.283734, -95.534203 50.284, -95.53447 50.284264, -95.534652 50.28453, -95.534835 50.284796, -95.535018 50.285062, -95.5352 50.285328, -95.535383 50.285594, -95.535565 50.285859, -95.535832 50.286123, -95.536099 50.286387, -95.53645 50.28665, -95.536801 50.286912, -95.537152 50.287174, -95.537418 50.287438, -95.537601 50.287704, -95.5377 50.287972, -95.537798 50.288239, -95.537897 50.288507, -95.537995 50.288775, -95.538093 50.289043, -95.538192 50.28931, -95.538206 50.28958, -95.538221 50.289849, -95.538235 50.290119, -95.538334 50.290387, -95.538432 50.290654, -95.538531 50.290922, -95.538629 50.29119)",m \ No newline at end of file diff --git a/tests/test_data/api_query_results_geojson.json b/tests/test_data/api_query_results_geojson.json new file mode 100644 index 00000000..84bd077e --- /dev/null +++ b/tests/test_data/api_query_results_geojson.json @@ -0,0 +1,1147 @@ +{ + "type": "FeatureCollection", + "features": [ + { + "id": "0", + "properties": { + "reach_id": "71224100223", + "time_str": "2023-06-10T19:39:43Z", + "wse": "286.2983", + "sword_version": "15", + "collection_shortname": "SWOT_L2_HR_RiverSP_2.0", + "crid": "PIA1", + "wse_units": "m" + }, + "geometry": { + "coordinates": [ + [ + -95.564991, + 50.223686 + ], + [ + -95.564559, + 50.223479 + ], + [ + -95.564133, + 50.223381 + ], + [ + -95.563713, + 50.22339 + ], + [ + -95.563296, + 50.223453 + ], + [ + -95.562884, + 50.223624 + ], + [ + -95.562473, + 50.223795 + ], + [ + -95.562062, + 50.223966 + ], + [ + -95.56165, + 50.224137 + ], + [ + -95.561242, + 50.224362 + ], + [ + -95.560917, + 50.224585 + ], + [ + -95.560595, + 50.224862 + ], + [ + -95.560271, + 50.225085 + ], + [ + -95.559946, + 50.225308 + ], + [ + -95.559946, + 50.225308 + ], + [ + -95.559213, + 50.225756 + ], + [ + -95.558804, + 50.225981 + ], + [ + -95.558567, + 50.226256 + ], + [ + -95.558413, + 50.226529 + ], + [ + -95.558343, + 50.226801 + ], + [ + -95.558274, + 50.227072 + ], + [ + -95.558288, + 50.227342 + ], + [ + -95.558303, + 50.227611 + ], + [ + -95.558317, + 50.227881 + ], + [ + -95.558416, + 50.228148 + ], + [ + -95.558514, + 50.228416 + ], + [ + -95.558697, + 50.228682 + ], + [ + -95.558795, + 50.22895 + ], + [ + -95.558978, + 50.229216 + ], + [ + -95.559076, + 50.229483 + ], + [ + -95.559259, + 50.229749 + ], + [ + -95.559357, + 50.230017 + ], + [ + -95.559455, + 50.230284 + ], + [ + -95.55947, + 50.230554 + ], + [ + -95.559484, + 50.230823 + ], + [ + -95.559583, + 50.231091 + ], + [ + -95.559765, + 50.231357 + ], + [ + -95.559864, + 50.231625 + ], + [ + -95.559878, + 50.231894 + ], + [ + -95.559809, + 50.232166 + ], + [ + -95.559571, + 50.232441 + ], + [ + -95.559165, + 50.23272 + ], + [ + -95.558757, + 50.232944 + ], + [ + -95.558348, + 50.233169 + ], + [ + -95.557939, + 50.233394 + ], + [ + -95.55753, + 50.233619 + ], + [ + -95.557206, + 50.233842 + ], + [ + -95.556884, + 50.234119 + ], + [ + -95.556562, + 50.234396 + ], + [ + -95.556241, + 50.234673 + ], + [ + -95.556003, + 50.234948 + ], + [ + -95.555681, + 50.235225 + ], + [ + -95.555443, + 50.2355 + ], + [ + -95.555206, + 50.235775 + ], + [ + -95.555136, + 50.236047 + ], + [ + -95.555066, + 50.236318 + ], + [ + -95.555081, + 50.236588 + ], + [ + -95.555011, + 50.236859 + ], + [ + -95.554941, + 50.23713 + ], + [ + -95.554701, + 50.237351 + ], + [ + -95.554376, + 50.237575 + ], + [ + -95.554052, + 50.237798 + ], + [ + -95.553727, + 50.238021 + ], + [ + -95.553727, + 50.238021 + ], + [ + -95.55308, + 50.238521 + ], + [ + -95.552843, + 50.238796 + ], + [ + -95.552521, + 50.239073 + ], + [ + -95.552367, + 50.239346 + ], + [ + -95.552297, + 50.239617 + ], + [ + -95.552312, + 50.239887 + ], + [ + -95.552326, + 50.240156 + ], + [ + -95.552425, + 50.240424 + ], + [ + -95.552439, + 50.240693 + ], + [ + -95.552453, + 50.240963 + ], + [ + -95.552468, + 50.241233 + ], + [ + -95.552482, + 50.241502 + ], + [ + -95.552497, + 50.241772 + ], + [ + -95.552511, + 50.242041 + ], + [ + -95.552525, + 50.242311 + ], + [ + -95.552456, + 50.242582 + ], + [ + -95.552299, + 50.242801 + ], + [ + -95.552056, + 50.242969 + ], + [ + -95.551728, + 50.243138 + ], + [ + -95.551314, + 50.243255 + ], + [ + -95.550899, + 50.243372 + ], + [ + -95.550487, + 50.243543 + ], + [ + -95.550076, + 50.243714 + ], + [ + -95.549661, + 50.243831 + ], + [ + -95.549249, + 50.244002 + ], + [ + -95.548838, + 50.244173 + ], + [ + -95.548423, + 50.24429 + ], + [ + -95.548011, + 50.244461 + ], + [ + -95.547603, + 50.244686 + ], + [ + -95.547278, + 50.244909 + ], + [ + -95.546953, + 50.245132 + ], + [ + -95.546715, + 50.245407 + ], + [ + -95.546561, + 50.24568 + ], + [ + -95.546492, + 50.245951 + ], + [ + -95.546422, + 50.246223 + ], + [ + -95.546436, + 50.246492 + ], + [ + -95.546367, + 50.246764 + ], + [ + -95.546297, + 50.247035 + ], + [ + -95.546143, + 50.247308 + ], + [ + -95.54599, + 50.247582 + ], + [ + -95.545752, + 50.247857 + ], + [ + -95.545598, + 50.24813 + ], + [ + -95.545444, + 50.248403 + ], + [ + -95.545374, + 50.248674 + ], + [ + -95.545305, + 50.248946 + ], + [ + -95.545319, + 50.249215 + ], + [ + -95.545333, + 50.249485 + ], + [ + -95.545348, + 50.249754 + ], + [ + -95.545446, + 50.250022 + ], + [ + -95.545545, + 50.25029 + ], + [ + -95.545727, + 50.250556 + ], + [ + -95.54591, + 50.250822 + ], + [ + -95.546176, + 50.251086 + ], + [ + -95.546359, + 50.251351 + ], + [ + -95.546625, + 50.251615 + ], + [ + -95.546892, + 50.251879 + ], + [ + -95.547075, + 50.252145 + ], + [ + -95.547257, + 50.252411 + ], + [ + -95.54744, + 50.252677 + ], + [ + -95.547538, + 50.252945 + ], + [ + -95.547553, + 50.253214 + ], + [ + -95.547651, + 50.253482 + ], + [ + -95.547581, + 50.253753 + ], + [ + -95.547512, + 50.254025 + ], + [ + -95.547442, + 50.254296 + ], + [ + -95.547372, + 50.254568 + ], + [ + -95.547303, + 50.254839 + ], + [ + -95.547317, + 50.255108 + ], + [ + -95.547247, + 50.25538 + ], + [ + -95.547177, + 50.255651 + ], + [ + -95.547192, + 50.255921 + ], + [ + -95.547206, + 50.25619 + ], + [ + -95.547221, + 50.25646 + ], + [ + -95.547319, + 50.256728 + ], + [ + -95.547418, + 50.256995 + ], + [ + -95.547432, + 50.257265 + ], + [ + -95.547446, + 50.257534 + ], + [ + -95.547461, + 50.257804 + ], + [ + -95.547475, + 50.258073 + ], + [ + -95.547489, + 50.258343 + ], + [ + -95.547504, + 50.258613 + ], + [ + -95.547518, + 50.258882 + ], + [ + -95.547449, + 50.259153 + ], + [ + -95.547379, + 50.259425 + ], + [ + -95.547309, + 50.259696 + ], + [ + -95.547239, + 50.259968 + ], + [ + -95.547086, + 50.260241 + ], + [ + -95.547016, + 50.260512 + ], + [ + -95.546946, + 50.260784 + ], + [ + -95.546876, + 50.261055 + ], + [ + -95.546807, + 50.261326 + ], + [ + -95.546737, + 50.261598 + ], + [ + -95.546583, + 50.261871 + ], + [ + -95.546345, + 50.262146 + ], + [ + -95.54602, + 50.262369 + ], + [ + -95.545611, + 50.262594 + ], + [ + -95.5452, + 50.262765 + ], + [ + -95.544788, + 50.262936 + ], + [ + -95.544373, + 50.263053 + ], + [ + -95.543961, + 50.263224 + ], + [ + -95.543546, + 50.263341 + ], + [ + -95.543135, + 50.263512 + ], + [ + -95.542723, + 50.263683 + ], + [ + -95.542314, + 50.263907 + ], + [ + -95.541989, + 50.26413 + ], + [ + -95.541667, + 50.264407 + ], + [ + -95.541345, + 50.264684 + ], + [ + -95.541107, + 50.264959 + ], + [ + -95.540869, + 50.265234 + ], + [ + -95.540631, + 50.265509 + ], + [ + -95.540393, + 50.265785 + ], + [ + -95.540155, + 50.26606 + ], + [ + -95.539917, + 50.266335 + ], + [ + -95.539679, + 50.26661 + ], + [ + -95.539441, + 50.266885 + ], + [ + -95.539203, + 50.26716 + ], + [ + -95.538962, + 50.267381 + ], + [ + -95.538634, + 50.26755 + ], + [ + -95.538304, + 50.267665 + ], + [ + -95.537889, + 50.267782 + ], + [ + -95.537471, + 50.267845 + ], + [ + -95.537056, + 50.267962 + ], + [ + -95.536642, + 50.268079 + ], + [ + -95.536227, + 50.268196 + ], + [ + -95.535809, + 50.268259 + ], + [ + -95.535391, + 50.268323 + ], + [ + -95.534974, + 50.268386 + ], + [ + -95.534556, + 50.268449 + ], + [ + -95.534138, + 50.268512 + ], + [ + -95.533718, + 50.268521 + ], + [ + -95.5333, + 50.268584 + ], + [ + -95.532882, + 50.268647 + ], + [ + -95.532552, + 50.268762 + ], + [ + -95.532224, + 50.268931 + ], + [ + -95.531986, + 50.269206 + ], + [ + -95.531748, + 50.269481 + ], + [ + -95.531594, + 50.269755 + ], + [ + -95.531356, + 50.27003 + ], + [ + -95.531202, + 50.270303 + ], + [ + -95.531048, + 50.270576 + ], + [ + -95.530978, + 50.270847 + ], + [ + -95.530908, + 50.271119 + ], + [ + -95.530923, + 50.271388 + ], + [ + -95.530937, + 50.271658 + ], + [ + -95.530951, + 50.271927 + ], + [ + -95.53105, + 50.272195 + ], + [ + -95.531148, + 50.272463 + ], + [ + -95.531331, + 50.272729 + ], + [ + -95.531513, + 50.272995 + ], + [ + -95.531696, + 50.273261 + ], + [ + -95.531878, + 50.273526 + ], + [ + -95.532145, + 50.27379 + ], + [ + -95.532327, + 50.274056 + ], + [ + -95.532594, + 50.27432 + ], + [ + -95.532861, + 50.274584 + ], + [ + -95.533043, + 50.27485 + ], + [ + -95.533142, + 50.275118 + ], + [ + -95.53324, + 50.275386 + ], + [ + -95.533339, + 50.275653 + ], + [ + -95.533437, + 50.275921 + ], + [ + -95.533536, + 50.276189 + ], + [ + -95.533634, + 50.276457 + ], + [ + -95.533732, + 50.276724 + ], + [ + -95.533747, + 50.276994 + ], + [ + -95.533761, + 50.277263 + ], + [ + -95.533859, + 50.277531 + ], + [ + -95.533958, + 50.277799 + ], + [ + -95.53414, + 50.278065 + ], + [ + -95.534323, + 50.278331 + ], + [ + -95.534506, + 50.278596 + ], + [ + -95.534688, + 50.278862 + ], + [ + -95.534871, + 50.279128 + ], + [ + -95.534969, + 50.279396 + ], + [ + -95.535152, + 50.279662 + ], + [ + -95.535334, + 50.279928 + ], + [ + -95.535433, + 50.280195 + ], + [ + -95.535615, + 50.280461 + ], + [ + -95.535798, + 50.280727 + ], + [ + -95.535812, + 50.280997 + ], + [ + -95.535743, + 50.281268 + ], + [ + -95.535589, + 50.281541 + ], + [ + -95.535266, + 50.281818 + ], + [ + -95.53486, + 50.282097 + ], + [ + -95.534454, + 50.282376 + ], + [ + -95.534132, + 50.282652 + ], + [ + -95.533893, + 50.282927 + ], + [ + -95.533824, + 50.283199 + ], + [ + -95.533838, + 50.283468 + ], + [ + -95.534021, + 50.283734 + ], + [ + -95.534203, + 50.284 + ], + [ + -95.53447, + 50.284264 + ], + [ + -95.534652, + 50.28453 + ], + [ + -95.534835, + 50.284796 + ], + [ + -95.535018, + 50.285062 + ], + [ + -95.5352, + 50.285328 + ], + [ + -95.535383, + 50.285594 + ], + [ + -95.535565, + 50.285859 + ], + [ + -95.535832, + 50.286123 + ], + [ + -95.536099, + 50.286387 + ], + [ + -95.53645, + 50.28665 + ], + [ + -95.536801, + 50.286912 + ], + [ + -95.537152, + 50.287174 + ], + [ + -95.537418, + 50.287438 + ], + [ + -95.537601, + 50.287704 + ], + [ + -95.5377, + 50.287972 + ], + [ + -95.537798, + 50.288239 + ], + [ + -95.537897, + 50.288507 + ], + [ + -95.537995, + 50.288775 + ], + [ + -95.538093, + 50.289043 + ], + [ + -95.538192, + 50.28931 + ], + [ + -95.538206, + 50.28958 + ], + [ + -95.538221, + 50.289849 + ], + [ + -95.538235, + 50.290119 + ], + [ + -95.538334, + 50.290387 + ], + [ + -95.538432, + 50.290654 + ], + [ + -95.538531, + 50.290922 + ], + [ + -95.538629, + 50.29119 + ] + ], + "type": "LineString" + }, + "type": "Feature" + } + ] +} \ No newline at end of file