Skip to content

Commit

Permalink
Implement unique valid values endpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
nickeopti committed Sep 2, 2021
1 parent 9bd5873 commit 357655f
Show file tree
Hide file tree
Showing 6 changed files with 148 additions and 0 deletions.
4 changes: 4 additions & 0 deletions terracotta/drivers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ def get_keys(self) -> OrderedDict:
"""
pass

@abstractmethod
def get_valid_values(self, where: Mapping[str, Union[str, List[str]]]) -> Dict[str, List[str]]:
pass

@abstractmethod
def get_datasets(self, where: Mapping[str, Union[str, List[str]]] = None,
page: int = 0, limit: int = None) -> Dict[Tuple[str, ...], Any]:
Expand Down
29 changes: 29 additions & 0 deletions terracotta/drivers/mysql.py
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,35 @@ def _get_keys(self) -> OrderedDict:

return out

@requires_connection
@convert_exceptions('Could not retrieve valid key values')
def get_valid_values(self, where: Mapping[str, Union[str, List[str]]]) -> Dict[str, List[str]]:
cursor = self._cursor

if not all(key in self.key_names for key in where.keys()):
raise exceptions.InvalidKeyError('Encountered unrecognized keys in where clause')

conditions = []
values = []
for key, value in where.items():
if isinstance(value, str):
value = [value]
values.extend(value)
conditions.append(' OR '.join([f'{key}=%s'] * len(value)))
where_fragment = ' AND '.join([f'({condition})' for condition in conditions])
where_fragment = ' WHERE ' + where_fragment if where_fragment else ''

valid_values = {key: [val] if isinstance(val, str) else val for key, val in where.items()}

for key in set(self.key_names) - set(where.keys()):
cursor.execute(
f'SELECT DISTINCT {key} FROM datasets {where_fragment}',
values
)
valid_values[key] = list([row[key] for row in cursor.fetchall()])

return valid_values

@trace('get_datasets')
@requires_connection
@convert_exceptions('Could not retrieve datasets')
Expand Down
29 changes: 29 additions & 0 deletions terracotta/drivers/sqlite.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,35 @@ def get_keys(self) -> OrderedDict:
out[row['key']] = row['description']
return out

@requires_connection
@convert_exceptions('Could not retrieve valid key values')
def get_valid_values(self, where: Mapping[str, Union[str, List[str]]]) -> Dict[str, List[str]]:
conn = self._connection

if not all(key in self.key_names for key in where.keys()):
raise exceptions.InvalidKeyError('Encountered unrecognized keys in where clause')

conditions = []
values = []
for key, value in where.items():
if isinstance(value, str):
value = [value]
values.extend(value)
conditions.append(' OR '.join([f'{key}=?'] * len(value)))
where_fragment = ' AND '.join([f'({condition})' for condition in conditions])
where_fragment = ' WHERE ' + where_fragment if where_fragment else ''

valid_values = {key: [val] if isinstance(val, str) else val for key, val in where.items()}

for key in set(self.key_names) - set(where.keys()):
rows = conn.execute(
f'SELECT DISTINCT {key} FROM datasets {where_fragment}',
values
)
valid_values[key] = list([row[key] for row in rows])

return valid_values

@trace('get_datasets')
@requires_connection
@convert_exceptions('Could not retrieve datasets')
Expand Down
21 changes: 21 additions & 0 deletions terracotta/handlers/valid_values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
"""handlers/valid_values.py
Handle /valid_values API endpoint.
"""

from typing import Dict, Mapping, List, Union

from terracotta import get_settings, get_driver
from terracotta.profile import trace


@trace('valid_values_handler')
def valid_values(some_keys: Mapping[str, Union[str, List[str]]] = None) -> Dict[str, List[str]]:
"""List all available valid values"""
settings = get_settings()
driver = get_driver(settings.DRIVER_PATH, provider=settings.DRIVER_PROVIDER)

with driver.connect():
valid_values = driver.get_valid_values(some_keys or {})

return valid_values
2 changes: 2 additions & 0 deletions terracotta/server/flask_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ def create_app(debug: bool = False, profile: bool = False) -> Flask:
from terracotta import get_settings
import terracotta.server.datasets
import terracotta.server.keys
import terracotta.server.valid_values
import terracotta.server.colormap
import terracotta.server.metadata
import terracotta.server.rgb
Expand Down Expand Up @@ -97,6 +98,7 @@ def create_app(debug: bool = False, profile: bool = False) -> Flask:
with new_app.test_request_context():
SPEC.path(view=terracotta.server.datasets.get_datasets)
SPEC.path(view=terracotta.server.keys.get_keys)
SPEC.path(view=terracotta.server.valid_values.get_valid_values)
SPEC.path(view=terracotta.server.colormap.get_colormap)
SPEC.path(view=terracotta.server.metadata.get_metadata)
SPEC.path(view=terracotta.server.rgb.get_rgb)
Expand Down
63 changes: 63 additions & 0 deletions terracotta/server/valid_values.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
"""server/valid_values.py
Flask route to handle /valid_values calls.
"""

from typing import Any, Dict, List, Union
from flask import request, jsonify, Response
from marshmallow import Schema, fields, INCLUDE, post_load
import re

from terracotta.server.flask_api import METADATA_API


class KeyValueOptionSchema(Schema):
class Meta:
unknown = INCLUDE

# placeholder values to document keys
key1 = fields.String(example='value1', description='Value of key1', dump_only=True)
key2 = fields.String(example='value2', description='Value of key2', dump_only=True)

@post_load
def list_items(self, data: Dict[str, Any], **kwargs: Any) -> Dict[str, Union[str, List[str]]]:
# Create lists of values supplied as stringified lists
for key, value in data.items():
if isinstance(value, str) and re.match(r'^\[.*\]$', value):
data[key] = value[1:-1].split(',')
return data


@METADATA_API.route('/valid_values', methods=['GET'])
def get_valid_values() -> Response:
"""Get all valid values combinations (possibly when given a value for some keys)
---
get:
summary: /datasets
description:
Get keys of all available datasets that match given key constraint.
Constraints may be combined freely. Returns all known datasets if no query parameters
are given.
parameters:
- in: query
schema: DatasetOptionSchema
responses:
200:
description: All available key combinations
schema:
type: array
items: DatasetSchema
400:
description: Query parameters contain unrecognized keys
"""
from terracotta.handlers.valid_values import valid_values
option_schema = KeyValueOptionSchema()
options = option_schema.load(request.args)

keys = options or None

payload = {
'valid_values': valid_values(keys)
}

return jsonify(payload)

0 comments on commit 357655f

Please sign in to comment.