Skip to content

Commit

Permalink
WIP on dynamic base classes
Browse files Browse the repository at this point in the history
  • Loading branch information
chrisbc committed Jan 9, 2024
1 parent 5781a96 commit 5418d20
Show file tree
Hide file tree
Showing 4 changed files with 166 additions and 7 deletions.
45 changes: 40 additions & 5 deletions tests/test_pynamo_models_v3.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,16 @@
import unittest
import pytest

import os
from unittest import mock

import pynamodb.exceptions
from moto import mock_dynamodb
from nzshm_common.location.code_location import CodedLocation

from toshi_hazard_store import model
from toshi_hazard_store.v2.db_adapter.sqlite import SqliteAdapter
from toshi_hazard_store.model.openquake_models import ensure_class_bases_begin_with


def get_one_rlz():
Expand Down Expand Up @@ -35,16 +41,45 @@ def get_one_hazard_aggregate():
).set_location(location)


# ref https://docs.pytest.org/en/7.3.x/example/parametrize.html#deferring-the-setup-of-parametrized-resources
def pytest_generate_tests(metafunc):
if "adapted_model" in metafunc.fixturenames:
metafunc.parametrize("adapted_model", ["pynamodb", "sqlite"], indirect=True)


@pytest.fixture
def adapted_model(request, tmp_path):
if request.param == 'pynamodb':
with mock_dynamodb():
model.ToshiOpenquakeMeta.create_table(wait=True)
yield model
model.ToshiOpenquakeMeta.delete_table()
elif request.param == 'sqlite':
envvars = {"THS_SQLITE_FOLDER": str(tmp_path), "THS_USE_SQLITE_ADAPTER": "TRUE"}
with mock.patch.dict(os.environ, envvars, clear=True):
ensure_class_bases_begin_with(
namespace=model.__dict__,
class_name=str('ToshiOpenquakeMeta'), # `str` type differs on Python 2 vs. 3.
base_class=SqliteAdapter,
)
model.ToshiOpenquakeMeta.create_table(wait=True)
yield model
model.ToshiOpenquakeMeta.delete_table()
else:
raise ValueError("invalid internal test config")


# MAKE this test both pynamo and sqlite
class TestPynamoMeta(object):
def test_table_exists(self, adapter_model):
assert adapter_model.OpenquakeRealization.exists()
assert adapter_model.ToshiOpenquakeMeta.exists()
def test_table_exists(self, adapted_model):
# assert adapted_model.OpenquakeRealization.exists()
assert adapted_model.ToshiOpenquakeMeta.exists()

def test_save_one_meta_object(self, get_one_meta):
def test_save_one_meta_object(self, get_one_meta, adapted_model):
obj = get_one_meta
obj.save()
assert obj.inv_time == 1.0
# assert adapted_model == 2


@mock_dynamodb
Expand All @@ -60,7 +95,7 @@ def tearDown(self):

def test_table_exists(self):
self.assertEqual(model.OpenquakeRealization.exists(), True)
self.assertEqual(model.ToshiOpenquakeMeta.exists(), True)
# self.assertEqual(model.ToshiOpenquakeMeta.exists(), True)

def test_save_one_new_realization_object(self):
"""New realization handles all the IMT levels."""
Expand Down
4 changes: 4 additions & 0 deletions tests/v2/test_pynamo_models.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
import pytest


@pytest.mark.skip('DUP')
class TestPynamoMeta(object):
def test_meta_table_exists(self, adapter_model):
assert adapter_model.ToshiOpenquakeMeta.exists()
Expand Down
48 changes: 46 additions & 2 deletions toshi_hazard_store/model/openquake_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,19 +9,59 @@
from pynamodb.models import Model
from pynamodb_attributes import IntegerAttribute, TimestampAttribute

from toshi_hazard_store.config import DEPLOYMENT_STAGE, IS_OFFLINE, REGION
from toshi_hazard_store.config import DEPLOYMENT_STAGE, IS_OFFLINE, REGION, USE_SQLITE_ADAPTER
from toshi_hazard_store.model.caching import ModelCacheMixin

from .attributes import EnumConstrainedUnicodeAttribute, IMTValuesAttribute, LevelValuePairAttribute
from .constraints import AggregationEnum, IntensityMeasureTypeEnum
from .location_indexed_model import VS30_KEYLEN, LocationIndexedModel, datetime_now


from toshi_hazard_store.v2.db_adapter.sqlite import SqliteAdapter

# MODELBASE = SqliteAdapter if USE_SQLITE_ADAPTER else Model
# MODELCACHEBASE = SqliteAdapter if USE_SQLITE_ADAPTER else ModelCacheMixin

log = logging.getLogger(__name__)

# ref https://stackoverflow.com/a/28075525
def ensure_class_bases_begin_with(namespace, class_name, base_class):
"""Ensure the named class's bases start with the base class.
:param namespace: The namespace containing the class name.
:param class_name: The name of the class to alter.
:param base_class: The type to be the first base class for the
newly created type.
:return: ``None``.
Call this function after ensuring `base_class` is
available, before using the class named by `class_name`.
"""
existing_class = namespace[class_name]
assert isinstance(existing_class, type)

bases = list(existing_class.__bases__)
if base_class is bases[0]:
# Already bound to a type with the right bases.
return
bases.insert(0, base_class)

class ToshiOpenquakeMeta(Model):
new_class_namespace = existing_class.__dict__.copy()
# Type creation will assign the correct ‘__dict__’ attribute.
new_class_namespace.pop('__dict__', None)

metaclass = existing_class.__metaclass__
new_class = metaclass(class_name, tuple(bases), new_class_namespace)

namespace[class_name] = new_class


class ToshiOpenquakeMeta:
"""Stores metadata from the job configuration and the oq HDF5."""

__metaclass__ = type

class Meta:
"""DynamoDB Metadata."""

Expand Down Expand Up @@ -52,6 +92,10 @@ class Meta:
rlz_lt = JSONAttribute() # realization meta as DataFrame JSON


# set default otp pynamodb
ensure_class_bases_begin_with(namespace=globals(), class_name='ToshiOpenquakeMeta', base_class=Model)


class vs30_nloc1_gt_rlz_index(LocalSecondaryIndex):
"""
Local secondary index with vs#) + 0.1 Degree search resolution
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
# test_model_baseis_dynamic.py

import os
from unittest import mock

import pytest
from pynamodb.attributes import UnicodeAttribute
from pynamodb.models import Model
from pytest_lazyfixture import lazy_fixture

from toshi_hazard_store.v2.db_adapter.sqlite import SqliteAdapter

from toshi_hazard_store.model.openquake_models import ensure_class_bases_begin_with
from toshi_hazard_store import model


class MySqlModel:
__metaclass__ = type

class Meta:
table_name = "MySQLITEModel"

my_hash_key = UnicodeAttribute(hash_key=True)
my_range_key = UnicodeAttribute(range_key=True)


def test_dynamic_baseclass():
ensure_class_bases_begin_with(
namespace=globals(), # __name__.__dict__,
class_name=str('MySqlModel'), # `str` type differs on Python 2 vs. 3.
base_class=Model,
)

instance = MySqlModel(my_hash_key='A', my_range_key='B')
assert isinstance(instance, (MySqlModel, Model))

ensure_class_bases_begin_with(
namespace=globals(), # __name__.__dict__,
class_name=str('MySqlModel'), # `str` type differs on Python 2 vs. 3.
base_class=SqliteAdapter,
)

instance = MySqlModel(my_hash_key='A2', my_range_key='B2')
assert isinstance(instance, (MySqlModel, Model, SqliteAdapter))


@pytest.fixture(scope="module")
def sqlite_adapter_base():
yield SqliteAdapter


@pytest.fixture(scope="module")
def pynamodb_adapter_base():
yield Model


def test_dynamic_baseclass_adapter_sqlite(sqlite_adapter_base):
ensure_class_bases_begin_with(
namespace=model.__dict__,
class_name=str('ToshiOpenquakeMeta'), # `str` type differs on Python 2 vs. 3.
base_class=sqlite_adapter_base,
)

instance = MySqlModel(my_hash_key='A', my_range_key='B')
assert isinstance(instance, (MySqlModel, sqlite_adapter_base))


def test_dynamic_baseclass_adapter_pynamodb(pynamodb_adapter_base):
ensure_class_bases_begin_with(
namespace=model.__dict__,
class_name=str('ToshiOpenquakeMeta'), # `str` type differs on Python 2 vs. 3.
base_class=pynamodb_adapter_base,
)

instance = MySqlModel(my_hash_key='A', my_range_key='B')
assert isinstance(instance, (MySqlModel, pynamodb_adapter_base))

0 comments on commit 5418d20

Please sign in to comment.