Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add writable flag to meta stores #256

Merged
merged 15 commits into from
Apr 1, 2022
14 changes: 14 additions & 0 deletions terracotta/drivers/base_classes.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,25 @@
from typing import (Any, Callable, Dict, List, Mapping, Optional, Sequence,
Tuple, TypeVar, Union)

from terracotta import exceptions

KeysType = Mapping[str, str]
MultiValueKeysType = Mapping[str, Union[str, List[str]]]
Number = TypeVar('Number', int, float)
T = TypeVar('T')


def requires_writable(fun: Callable[..., T]) -> Callable[..., T]:
@functools.wraps(fun)
def inner(self: MetaStore, *args: Any, **kwargs: Any) -> T:
if self._WRITABLE:
return fun(self, *args, **kwargs)
else:
raise exceptions.DatabaseNotWritableError("Database not writable")

return inner


def requires_connection(
fun: Callable[..., T] = None, *,
verify: bool = True
Expand All @@ -38,6 +51,7 @@ class MetaStore(ABC):
Defines a common interface for all metadata backends.
"""
_RESERVED_KEYS = ('limit', 'page')
_WRITABLE: bool = True

@property
@abstractmethod
Expand Down
6 changes: 5 additions & 1 deletion terracotta/drivers/relational_meta_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@
from terracotta import exceptions
from terracotta.drivers.base_classes import (KeysType, MetaStore,
MultiValueKeysType,
requires_connection)
requires_connection,
requires_writable)
from terracotta.profile import trace

_ERROR_ON_CONNECT = (
Expand Down Expand Up @@ -181,6 +182,7 @@ def db_version(self) -> str:
version = self.connection.execute(stmt).scalar()
return version

@requires_writable
@convert_exceptions('Could not create database')
def create(self, keys: Sequence[str], key_descriptions: Mapping[str, str] = None) -> None:
"""Create and initialize database with empty tables.
kiksekage marked this conversation as resolved.
Show resolved Hide resolved
Expand Down Expand Up @@ -349,6 +351,7 @@ def get_metadata(self, keys: KeysType) -> Optional[Dict[str, Any]]:
return self._decode_data(encoded_data)

@trace('insert')
@requires_writable
@requires_connection
@convert_exceptions('Could not write to database')
def insert(
Expand Down Expand Up @@ -383,6 +386,7 @@ def insert(
)

@trace('delete')
@requires_writable
@requires_connection
@convert_exceptions('Could not write to database')
def delete(self, keys: KeysType) -> None:
Expand Down
15 changes: 11 additions & 4 deletions terracotta/drivers/sqlite_remote_meta_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@

from terracotta import exceptions, get_settings
from terracotta.drivers.sqlite_meta_store import SQLiteMetaStore
from terracotta.drivers.base_classes import requires_writable
from terracotta.profile import trace

logger = logging.getLogger(__name__)
Expand Down Expand Up @@ -69,10 +70,12 @@ class RemoteSQLiteMetaStore(SQLiteMetaStore):
Warning:

This driver is read-only. Any attempts to use the create, insert, or delete methods
will throw a NotImplementedError.
will throw a DatabaseNotWritableError.

"""

_WRITABLE: bool = False

def __init__(self, remote_path: Union[str, Path]) -> None:
"""Initialize the RemoteSQLiteDriver.

Expand Down Expand Up @@ -132,14 +135,18 @@ def _connection_callback(self) -> None:
self._update_db(self._remote_path, self._local_path)
super()._connection_callback()

# Always raises DatabaseNotWritableError
@requires_writable
kiksekage marked this conversation as resolved.
Show resolved Hide resolved
def create(self, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError('Remote SQLite databases are read-only')
pass

@requires_writable
def insert(self, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError('Remote SQLite databases are read-only')
pass

@requires_writable
def delete(self, *args: Any, **kwargs: Any) -> None:
raise NotImplementedError('Remote SQLite databases are read-only')
pass
kiksekage marked this conversation as resolved.
Show resolved Hide resolved

def __del__(self) -> None:
"""Clean up temporary database upon exit"""
Expand Down
12 changes: 9 additions & 3 deletions terracotta/drivers/terracotta_driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

import contextlib
from collections import OrderedDict
from typing import (Any, Collection, Dict, List, Mapping, Optional, Sequence, Tuple, TypeVar,
Union)
from typing import (Any, Collection, Dict, List, Mapping,
Optional, Sequence, Tuple, TypeVar, Union)

import terracotta
from terracotta import exceptions
Expand Down Expand Up @@ -169,7 +169,13 @@ def get_metadata(self, keys: ExtendedKeysType) -> Dict[str, Any]:

path = squeeze(dataset.values())
metadata = self.compute_metadata(path, max_shape=self.LAZY_LOADING_MAX_SHAPE)
self.insert(keys, path, metadata=metadata)

try:
self.insert(keys, path, metadata=metadata)
kiksekage marked this conversation as resolved.
Show resolved Hide resolved
except exceptions.DatabaseNotWritableError as exc:
raise exceptions.DatabaseNotWritableError(
"Lazy loading requires a writable database"
) from exc
dionhaefner marked this conversation as resolved.
Show resolved Hide resolved

# ensure standardized/consistent output (types and floating point precision)
metadata = self.meta_store.get_metadata(keys)
Expand Down
4 changes: 4 additions & 0 deletions terracotta/exceptions.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,5 +24,9 @@ class InvalidDatabaseError(Exception):
pass


class DatabaseNotWritableError(Exception):
pass


class PerformanceWarning(UserWarning):
pass
18 changes: 18 additions & 0 deletions tests/drivers/test_raster_drivers.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import numpy as np

from terracotta import exceptions

DRIVERS = ['sqlite', 'mysql']
METADATA_KEYS = ('bounds', 'range', 'mean', 'stdev', 'percentiles', 'metadata')

Expand Down Expand Up @@ -144,6 +146,22 @@ def test_lazy_loading(driver_path, provider, raster_file):
assert all(np.all(data1[k] == data2[k]) for k in data1.keys())


@pytest.mark.parametrize('provider', DRIVERS)
def test_non_writable_lazy_loading(driver_path, provider, raster_file):
from terracotta import drivers
db = drivers.get_driver(driver_path, provider=provider)
keys = ('some', 'keynames')

db.create(keys)
db.insert(['some', 'value'], str(raster_file), skip_metadata=True)

# Manually set the meta store to un-writable
db.meta_store._WRITABLE = False

with pytest.raises(exceptions.DatabaseNotWritableError):
db.get_metadata(['some', 'value'])
kiksekage marked this conversation as resolved.
Show resolved Hide resolved


@pytest.mark.parametrize('provider', DRIVERS)
def test_precomputed_metadata(driver_path, provider, raster_file):
from terracotta import drivers
Expand Down
8 changes: 5 additions & 3 deletions tests/drivers/test_sqlite_remote.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@

import pytest

from terracotta import exceptions

boto3 = pytest.importorskip('boto3')
moto = pytest.importorskip('moto')

Expand Down Expand Up @@ -142,13 +144,13 @@ def test_immutability(s3_db_factory, raster_file):

driver = get_driver(dbpath)

with pytest.raises(NotImplementedError):
with pytest.raises(exceptions.DatabaseNotWritableError):
driver.create(keys)

with pytest.raises(NotImplementedError):
with pytest.raises(exceptions.DatabaseNotWritableError):
driver.insert(('some', 'value'), str(raster_file))

with pytest.raises(NotImplementedError):
with pytest.raises(exceptions.DatabaseNotWritableError):
driver.delete(('some', 'value'))


Expand Down