Skip to content

Commit

Permalink
Merge pull request #14 from gumo-py/20190705-refactoring-configuratio…
Browse files Browse the repository at this point in the history
…n-dependencies

Refactoring configuration dependencies
  • Loading branch information
akiray03 committed Jul 5, 2019
2 parents 2c74372 + ad9934c commit f6ea5ac
Show file tree
Hide file tree
Showing 8 changed files with 161 additions and 76 deletions.
2 changes: 1 addition & 1 deletion gumo/datastore/__init__.py
Expand Up @@ -4,7 +4,7 @@
from gumo.core import EntityKeyFactory

from gumo.datastore._configuration import configure
from gumo.datastore.domain.configuration import DatastoreConfiguration
from gumo.datastore.infrastructure.configuration import DatastoreConfiguration

from gumo.datastore.infrastructure.repository import datastore_transaction

Expand Down
19 changes: 3 additions & 16 deletions gumo/datastore/_configuration.py
@@ -1,11 +1,11 @@
import os
from logging import getLogger
from injector import singleton

from typing import Optional
from typing import Union

from gumo.core.injector import injector
from gumo.datastore.domain.configuration import DatastoreConfiguration
from gumo.datastore.infrastructure.configuration import DatastoreConfiguration


logger = getLogger('gumo.datastore')
Expand Down Expand Up @@ -43,21 +43,8 @@ def configure(
namespace=namespace,
)

if config.use_local_emulator:
if 'DATASTORE_EMULATOR_HOST' not in os.environ:
raise RuntimeError(
f'The environment variable "DATASTORE_EMULATOR_HOST" is required when using a datastore emulator.'
)
if os.environ['DATASTORE_EMULATOR_HOST'] != config.emulator_host:
host = os.environ['DATASTORE_EMULATOR_HOST']
raise RuntimeError(
f'Env-var "env["DATASTORE_EMULATOR_HOST"] and config.emulator_host are not corrected. '
f'env["DATASTORE_EMULATOR_HOST"]={host}, config.emulator_host={config.emulator_host}'
)

logger.debug(f'Gumo.Datastore is configured, config={config}')

injector.binder.bind(DatastoreConfiguration, to=config)
injector.binder.bind(DatastoreConfiguration, to=config, scope=singleton)

from gumo.datastore._bind import datastore_binder
injector.binder.install(datastore_binder)
Expand Down
15 changes: 5 additions & 10 deletions gumo/datastore/domain/configuration/__init__.py
@@ -1,13 +1,8 @@
import dataclasses
from typing import Optional
# backward compatibility

from gumo.datastore.infrastructure.configuration import DatastoreConfiguration

@dataclasses.dataclass(frozen=True)
class DatastoreConfiguration:
use_local_emulator: bool = False
emulator_host: Optional[str] = None
namespace: Optional[str] = None

def __post_init__(self):
if self.use_local_emulator and self.emulator_host is None:
raise ValueError(f'If the emulator enabled, then emulator_host must be present.')
__all__ = [
DatastoreConfiguration.__name__
]
75 changes: 75 additions & 0 deletions gumo/datastore/infrastructure/configuration/__init__.py
@@ -0,0 +1,75 @@
import os
import dataclasses
import threading

from typing import Optional
from typing import ClassVar
from typing import Union

from gumo.core import GoogleCloudProjectID

from google.cloud import datastore


@dataclasses.dataclass(frozen=False)
class DatastoreConfiguration:
google_cloud_project: Union[GoogleCloudProjectID, str, None] = None
use_local_emulator: bool = False
emulator_host: Optional[str] = None
namespace: Optional[str] = None
client: Optional[datastore.Client] = None

_ENV_KEY_GOOGLE_CLOUD_PROJECT: ClassVar = 'GOOGLE_CLOUD_PROJECT'
_ENV_KEY_DATASTORE_EMULATOR_HOST: ClassVar = 'DATASTORE_EMULATOR_HOST'

_lock: ClassVar = threading.Lock()

def __post_init__(self):
with self._lock:
self._set_google_cloud_project()
self._set_emulator_config()
self._set_client()

def _set_google_cloud_project(self):
if isinstance(self.google_cloud_project, str):
self.google_cloud_project = GoogleCloudProjectID(self.google_cloud_project)
if isinstance(self.google_cloud_project, GoogleCloudProjectID):
if self.google_cloud_project.value != os.environ.get(self._ENV_KEY_GOOGLE_CLOUD_PROJECT):
raise RuntimeError(f'Env-var "{self._ENV_KEY_GOOGLE_CLOUD_PROJECT}" is invalid or undefined.'
f'Please set value "{self.google_cloud_project.value}" to env-vars.')

if self.google_cloud_project is None:
if self._ENV_KEY_GOOGLE_CLOUD_PROJECT in os.environ:
self.google_cloud_project = GoogleCloudProjectID(os.environ[self._ENV_KEY_GOOGLE_CLOUD_PROJECT])
else:
raise RuntimeError(f'Env-var "{self._ENV_KEY_GOOGLE_CLOUD_PROJECT}" is undefined, please set it.')

def _set_emulator_config(self):
emulator_not_configured = not self.use_local_emulator and self.emulator_host is None
if emulator_not_configured and os.environ.get(self._ENV_KEY_DATASTORE_EMULATOR_HOST):
self.use_local_emulator = True
self.emulator_host = os.environ[self._ENV_KEY_DATASTORE_EMULATOR_HOST]

if not self.use_local_emulator:
return

if os.environ.get(self._ENV_KEY_DATASTORE_EMULATOR_HOST) is None:
raise RuntimeError(
f'If the emulator enabled, then env-var "{self._ENV_KEY_DATASTORE_EMULATOR_HOST}" must be present.'
)

if os.environ.get(self._ENV_KEY_DATASTORE_EMULATOR_HOST) != self.emulator_host:
host = os.environ.get(self._ENV_KEY_DATASTORE_EMULATOR_HOST)
raise RuntimeError(
f'Env-var "{self._ENV_KEY_DATASTORE_EMULATOR_HOST}" and self.emulator_host do not match. '
f'env["{self._ENV_KEY_DATASTORE_EMULATOR_HOST}"]={host}, self.emulator_host={self.emulator_host}'
)

def _set_client(self):
if isinstance(self.client, datastore.Client):
return

self.client = datastore.Client(
project=self.google_cloud_project.value,
namespace=self.namespace,
)
6 changes: 3 additions & 3 deletions gumo/datastore/infrastructure/key_id_allocator/__init__.py
Expand Up @@ -11,7 +11,7 @@
from gumo.core.domain.entity_key import EntityKeyFactory
from gumo.core.domain.entity_key import IncompleteKey

from gumo.datastore.infrastructure.repository import DatastoreClientFactory
from gumo.datastore.infrastructure.configuration import DatastoreConfiguration

from google.cloud import datastore

Expand Down Expand Up @@ -60,8 +60,8 @@ def __init__(
@property
def datastore_client(self) -> datastore.Client:
if self._datastore_client is None:
factory = injector.get(DatastoreClientFactory) # type: DatastoreClientFactory
self._datastore_client = factory.build()
configuration = injector.get(DatastoreConfiguration) # type: DatastoreConfiguration
self._datastore_client = configuration.client

return self._datastore_client

Expand Down
29 changes: 4 additions & 25 deletions gumo/datastore/infrastructure/repository/__init__.py
@@ -1,33 +1,12 @@
from injector import inject
from contextlib import contextmanager

from gumo.core.injector import injector

from gumo.core import GumoConfiguration
from gumo.datastore import DatastoreConfiguration

from gumo.datastore.infrastructure.configuration import DatastoreConfiguration
from gumo.datastore.infrastructure.entity_key_mapper import EntityKeyMapper

from google.cloud import datastore


class DatastoreClientFactory:
@inject
def __init__(
self,
gumo_config: GumoConfiguration,
datastore_config: DatastoreConfiguration
):
self._gumo_config = gumo_config
self._datastore_config = datastore_config

def build(self) -> datastore.Client:
return datastore.Client(
project=self._gumo_config.google_cloud_project.value,
namespace=self._datastore_config.namespace,
)


class DatastoreRepositoryMixin:
_datastore_client = None
_entity_key_mapper = None
Expand All @@ -37,8 +16,8 @@ class DatastoreRepositoryMixin:
@property
def datastore_client(self) -> datastore.Client:
if self._datastore_client is None:
factory = injector.get(DatastoreClientFactory) # type: DatastoreClientFactory
self._datastore_client = factory.build()
configuration = injector.get(DatastoreConfiguration) # type: DatastoreConfiguration
self._datastore_client = configuration.client

return self._datastore_client

Expand All @@ -52,7 +31,7 @@ def entity_key_mapper(self) -> EntityKeyMapper:

@contextmanager
def datastore_transaction():
datastore_client = injector.get(DatastoreClientFactory).build() # type: datastore.Client
datastore_client = injector.get(DatastoreConfiguration).client # type: datastore.Client

with datastore_client.transaction():
yield
2 changes: 1 addition & 1 deletion setup.py
Expand Up @@ -2,7 +2,7 @@


name = 'gumo-datastore'
version = '0.0.16'
version = '0.0.17'
description = 'Gumo Datastore Library'
dependencies = [
'gumo-core >= 0.0.31',
Expand Down
89 changes: 69 additions & 20 deletions tests/datastore/configure_test.py
@@ -1,27 +1,76 @@
import pytest
import os

from gumo.datastore._configuration import ConfigurationFactory
from gumo.datastore.domain.configuration import DatastoreConfiguration
from gumo.datastore.infrastructure.configuration import DatastoreConfiguration

from google.cloud import datastore

def test_configuration_factory_build():
o = ConfigurationFactory.build(
use_local_emulator='yes',
emulator_host='localhost:8080',
namespace='test',
)

assert o == DatastoreConfiguration(
use_local_emulator=True,
emulator_host='localhost:8080',
namespace='test',
)
class TestDatastoreConfiguration:
def setup_method(self, method):
self.env_vars = {}
for k, v in os.environ.items():
self.env_vars[k] = v

def teardown_method(self, method):
for k in os.environ.keys():
if k not in self.env_vars:
del os.environ[k]

def test_configuration_factory_build_failed():
with pytest.raises(ValueError):
ConfigurationFactory.build(
use_local_emulator='yes',
emulator_host=None,
namespace='test',
)
for k, v in self.env_vars.items():
os.environ[k] = v

def test_build_success_with_standard_configuration(self):
if 'DATASTORE_EMULATOR_HOST' in os.environ:
del os.environ['DATASTORE_EMULATOR_HOST']
assert os.environ['GOOGLE_CLOUD_PROJECT'] is not None
assert 'DATASTORE_EMULATOR_HOST' not in os.environ

o = DatastoreConfiguration()

assert o.google_cloud_project.value == os.environ['GOOGLE_CLOUD_PROJECT']
assert not o.use_local_emulator
assert o.emulator_host is None
assert o.namespace is None
assert isinstance(o.client, datastore.Client)

def test_build_success_with_emulator_configuration(self):
assert os.environ['GOOGLE_CLOUD_PROJECT'] is not None
assert os.environ['DATASTORE_EMULATOR_HOST'] is not None

o = DatastoreConfiguration()

assert o.google_cloud_project.value == os.environ['GOOGLE_CLOUD_PROJECT']
assert o.use_local_emulator
assert o.emulator_host == os.environ['DATASTORE_EMULATOR_HOST']
assert o.namespace is None
assert isinstance(o.client, datastore.Client)

def test_build_failure_without_google_cloud_project_env_vars(self):
if 'GOOGLE_CLOUD_PROJECT' in os.environ:
del os.environ['GOOGLE_CLOUD_PROJECT']

with pytest.raises(RuntimeError, match='"GOOGLE_CLOUD_PROJECT" is undefined'):
DatastoreConfiguration()

def test_build_failure_with_emulator_configuration_and_without_emulator_env_vars(self):
if 'DATASTORE_EMULATOR_HOST' in os.environ:
del os.environ['DATASTORE_EMULATOR_HOST']
assert os.environ['GOOGLE_CLOUD_PROJECT'] is not None
assert 'DATASTORE_EMULATOR_HOST' not in os.environ

with pytest.raises(RuntimeError, match='env-var "DATASTORE_EMULATOR_HOST" must be present'):
DatastoreConfiguration(
use_local_emulator=True
)

def test_build_failure_with_emulator_host_mismatched(self):
assert os.environ['GOOGLE_CLOUD_PROJECT'] is not None
assert os.environ['DATASTORE_EMULATOR_HOST'] is not None

with pytest.raises(RuntimeError,
match='Env-var "DATASTORE_EMULATOR_HOST" and self.emulator_host do not match.'):
DatastoreConfiguration(
use_local_emulator=True,
emulator_host='example.localhost:12345'
)

0 comments on commit f6ea5ac

Please sign in to comment.