Skip to content

Commit

Permalink
feat: Refactor redshift fixture to be "first class".
Browse files Browse the repository at this point in the history
Previously, redshift just directly used postgres' underlying
`_postgres_container` fixture. This means that you cannot simultaneously
use postgres AND redshift on different postgres database servers
(version/port would be common examples of things you might want to
customize).

Now, a separate `_redshift_container` and `RedshiftConfig` have been
added. By default they use the same values as the default postgres one;
which means that by default there will be no behavior change. A redshift
and postgres fixture, used together, will end up making use of the same
database server. However this change **enables** these configurations
to diverge for a project and use different config.

Additionally, postgres has had a lot more active development of its
fixture features. Redshift, using the postgres container, *can* make use
of all of this development, but had a completely unique implementation,
and so it did not.
  • Loading branch information
DanCardin committed Feb 23, 2022
1 parent 96da39a commit a4e71ee
Show file tree
Hide file tree
Showing 14 changed files with 217 additions and 101 deletions.
17 changes: 15 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,23 @@
# Changelog

## [Unreleased](https://github.com/schireson/schireson-pytest-mock-resources/compare/v2.1.12...HEAD) (2022-02-13)
### [v2.2.3](https://github.com/schireson/schireson-pytest-mock-resources/compare/v2.2.2...v2.2.3) (2022-02-23)

#### Features

* Refactor redshift fixture to be "first class". 1a3076d


### [v2.2.2](https://github.com/schireson/schireson-pytest-mock-resources/compare/v2.2.1...v2.2.2) (2022-02-23)


### [v2.2.1](https://github.com/schireson/schireson-pytest-mock-resources/compare/v2.2.0...v2.2.1) (2022-02-22)


## [v2.2.0](https://github.com/schireson/schireson-pytest-mock-resources/compare/v2.1.12...v2.2.0) (2022-02-14)

### Features

* Perform container cleanup in a multiprocess safe way. 28db0fb
* Perform container cleanup in a multiprocess safe way. 10a9946


### [v2.1.12](https://github.com/schireson/schireson-pytest-mock-resources/compare/v2.1.11...v2.1.12) (2022-02-08)
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ lint:
mypy src tests || exit 1

format:
isort --recursive src tests
isort src tests
black src tests

## Build
Expand All @@ -45,7 +45,7 @@ build-docs:
pip install -r docs/requirements.txt
make -C docs html

build: build-package build-docs
build: build-package

publish: build
poetry publish -u __token__ -p '${PYPI_PASSWORD}' --no-interaction
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[tool.poetry]
name = "pytest-mock-resources"
version = "2.2.2"
version = "2.2.3"
description = "A pytest plugin for easily instantiating reproducible mock resources."
authors = [
"Omar Khan <oakhan3@gmail.com>",
Expand Down
5 changes: 3 additions & 2 deletions src/pytest_mock_resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,15 @@
from pytest_mock_resources.container import (
_mongo_container,
_mysql_container,
_postgres_container,
_redis_container,
_redshift_container,
MongoConfig,
MysqlConfig,
PostgresConfig,
RedisConfig,
)
from pytest_mock_resources.fixture.database import (
_postgres_container,
_redshift_container,
create_mongo_fixture,
create_mysql_fixture,
create_postgres_fixture,
Expand All @@ -21,6 +21,7 @@
pmr_mysql_config,
pmr_postgres_config,
pmr_redis_config,
pmr_redshift_config,
Rows,
Statements,
)
Expand Down
3 changes: 1 addition & 2 deletions src/pytest_mock_resources/container/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# flake8: noqa
from pytest_mock_resources.container.mongo import _mongo_container, MongoConfig
from pytest_mock_resources.container.mysql import _mysql_container, MysqlConfig
from pytest_mock_resources.container.postgres import _postgres_container, PostgresConfig
from pytest_mock_resources.container.postgres import PostgresConfig
from pytest_mock_resources.container.redis import _redis_container, RedisConfig
from pytest_mock_resources.container.redshift import _redshift_container
30 changes: 9 additions & 21 deletions src/pytest_mock_resources/container/postgres.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import pytest
import sqlalchemy

from pytest_mock_resources.compat.sqlalchemy import URL
from pytest_mock_resources.config import DockerContainerConfig, fallback
from pytest_mock_resources.container.base import ContainerCheckFailed, get_container
from pytest_mock_resources.container.base import ContainerCheckFailed


class PostgresConfig(DockerContainerConfig):
Expand Down Expand Up @@ -50,21 +50,22 @@ def root_database(self):


def get_sqlalchemy_engine(config, database_name, **engine_kwargs):
URI_TEMPLATE = "postgresql+psycopg2://{username}:{password}@{host}:{port}/{database}?sslmode=disable"
DB_URI = URI_TEMPLATE.format(
url = URL(
drivername="postgresql+psycopg2",
host=config.host,
port=config.port,
username=config.username,
password=config.password,
database=database_name,
query={"sslmode": "disable"},
)

# Trigger any psycopg2-based import failures
from pytest_mock_resources.compat import psycopg2

psycopg2.connect

engine = sqlalchemy.create_engine(DB_URI, **engine_kwargs)
engine = sqlalchemy.create_engine(url, **engine_kwargs)

# Verify engine is connected
engine.connect()
Expand All @@ -77,20 +78,7 @@ def check_postgres_fn(config):
get_sqlalchemy_engine(config, config.root_database)
except sqlalchemy.exc.OperationalError:
raise ContainerCheckFailed(
"Unable to connect to a presumed Postgres test container via given config: {}".format(config)
"Unable to connect to a presumed Postgres test container via given config: {}".format(
config
)
)


@pytest.fixture(scope="session")
def _postgres_container(pytestconfig, pmr_postgres_config):
yield from get_container(
pytestconfig,
pmr_postgres_config,
ports={5432: pmr_postgres_config.port},
environment={
"POSTGRES_DB": pmr_postgres_config.root_database,
"POSTGRES_USER": pmr_postgres_config.username,
"POSTGRES_PASSWORD": pmr_postgres_config.password,
},
check_fn=check_postgres_fn,
)
62 changes: 58 additions & 4 deletions src/pytest_mock_resources/container/redshift.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,60 @@
import pytest
import sqlalchemy

from pytest_mock_resources.config import DockerContainerConfig, fallback
from pytest_mock_resources.container.base import ContainerCheckFailed
from pytest_mock_resources.container.postgres import get_sqlalchemy_engine

@pytest.fixture(scope="session")
def _redshift_container(_postgres_container):
return _postgres_container

class RedshiftConfig(DockerContainerConfig):
"""Define the configuration object for Redshift.
Args:
image (str): The docker image:tag specifier to use for Redshift containers.
Defaults to :code:`"postgres:9.6.10-alpine"`.
host (str): The hostname under which a mounted port will be available.
Defaults to :code:`"localhost"`.
port (int): The port to bind the container to.
Defaults to :code:`5532`.
ci_port (int): The port to bind the container to when a CI environment is detected.
Defaults to :code:`5432`.
username (str): The username of the root Redshift user
Defaults to :code:`"user"`.
password (str): The password of the root Redshift password
Defaults to :code:`"password"`.
root_database (str): The name of the root Redshift database to create.
Defaults to :code:`"dev"`.
"""

name = "redshift"
_fields = {"image", "host", "port", "ci_port", "username", "password", "root_database"}
_fields_defaults = {
"image": "postgres:9.6.10-alpine",
"port": 5532,
"ci_port": 5432,
"username": "user",
"password": "password",
"root_database": "dev",
}

@fallback
def username(self):
raise NotImplementedError()

@fallback
def password(self):
raise NotImplementedError()

@fallback
def root_database(self):
raise NotImplementedError()


def check_redshift_fn(config):
try:
get_sqlalchemy_engine(config, config.root_database)
except sqlalchemy.exc.OperationalError:
raise ContainerCheckFailed(
"Unable to connect to a presumed Redshift test container via given config: {}".format(
config
)
)
3 changes: 3 additions & 0 deletions src/pytest_mock_resources/fixture/database/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@
from pytest_mock_resources.fixture.database.mongo import create_mongo_fixture, pmr_mongo_config
from pytest_mock_resources.fixture.database.redis import create_redis_fixture, pmr_redis_config
from pytest_mock_resources.fixture.database.relational import (
_postgres_container,
_redshift_container,
create_mysql_fixture,
create_postgres_fixture,
create_redshift_fixture,
create_sqlite_fixture,
pmr_mysql_config,
pmr_postgres_config,
pmr_redshift_config,
Rows,
Statements,
)
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,13 @@
pmr_mysql_config,
)
from pytest_mock_resources.fixture.database.relational.postgresql import (
_postgres_container,
create_postgres_fixture,
pmr_postgres_config,
)
from pytest_mock_resources.fixture.database.relational.redshift import create_redshift_fixture
from pytest_mock_resources.fixture.database.relational.redshift import (
_redshift_container,
create_redshift_fixture,
pmr_redshift_config,
)
from pytest_mock_resources.fixture.database.relational.sqlite import create_sqlite_fixture
19 changes: 0 additions & 19 deletions src/pytest_mock_resources/fixture/database/relational/generic.py
Original file line number Diff line number Diff line change
Expand Up @@ -138,25 +138,6 @@ def create_ddl(self, metadata):
self._create_tables(metadata)
self._ddl_created = True

def manage(self, session=None):
try:
self._run_actions()

if session:
if isinstance(session, sessionmaker):
session_factory = session
else:
session_factory = sessionmaker(bind=self.engine)

Session = scoped_session(session_factory)
session = Session(bind=self.engine)
yield session
session.close()
else:
yield self.engine
finally:
self.engine.dispose()

def manage_sync(self, session=None):
try:
self._run_actions()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ def _(_mysql_container, pmr_mysql_config):
)

engine_manager = EngineManager(engine, ordered_actions, tables=tables)
for engine in engine_manager.manage(session=session):
for engine in engine_manager.manage_sync(session=session):
yield engine

return _
Expand Down
52 changes: 36 additions & 16 deletions src/pytest_mock_resources/fixture/database/relational/postgresql.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
import pytest
import sqlalchemy

from pytest_mock_resources.container.postgres import get_sqlalchemy_engine, PostgresConfig
from pytest_mock_resources.container.base import get_container
from pytest_mock_resources.container.postgres import (
check_postgres_fn,
get_sqlalchemy_engine,
PostgresConfig,
)
from pytest_mock_resources.fixture.database.generic import assign_fixture_credentials
from pytest_mock_resources.fixture.database.relational.generic import EngineManager

Expand All @@ -18,22 +23,19 @@ def pmr_postgres_config():
return PostgresConfig()


def create_engine_manager(
pmr_postgres_config, ordered_actions, tables, createdb_template="template1", engine_kwargs=None
):
database_name = produce_clean_database(pmr_postgres_config, createdb_template=createdb_template)

engine = get_sqlalchemy_engine(pmr_postgres_config, database_name, **(engine_kwargs or {}))
assign_fixture_credentials(
engine,
drivername="postgresql+psycopg2",
host=pmr_postgres_config.host,
port=pmr_postgres_config.port,
database=database_name,
username=pmr_postgres_config.username,
password=pmr_postgres_config.password,
@pytest.fixture(scope="session")
def _postgres_container(pytestconfig, pmr_postgres_config):
yield from get_container(
pytestconfig,
pmr_postgres_config,
ports={5432: pmr_postgres_config.port},
environment={
"POSTGRES_DB": pmr_postgres_config.root_database,
"POSTGRES_USER": pmr_postgres_config.username,
"POSTGRES_PASSWORD": pmr_postgres_config.password,
},
check_fn=check_postgres_fn,
)
return EngineManager(engine, ordered_actions, tables=tables, default_schema="public")


def create_postgres_fixture(
Expand Down Expand Up @@ -86,6 +88,24 @@ async def _async(_postgres_container, pmr_postgres_config):
return _sync


def create_engine_manager(
pmr_postgres_config, ordered_actions, tables, createdb_template="template1", engine_kwargs=None
):
database_name = produce_clean_database(pmr_postgres_config, createdb_template=createdb_template)

engine = get_sqlalchemy_engine(pmr_postgres_config, database_name, **(engine_kwargs or {}))
assign_fixture_credentials(
engine,
drivername="postgresql+psycopg2",
host=pmr_postgres_config.host,
port=pmr_postgres_config.port,
database=database_name,
username=pmr_postgres_config.username,
password=pmr_postgres_config.password,
)
return EngineManager(engine, ordered_actions, tables=tables, default_schema="public")


def produce_clean_database(config, createdb_template="template1"):
root_engine = get_sqlalchemy_engine(config, config.root_database, isolation_level="AUTOCOMMIT")
with root_engine.connect() as conn:
Expand Down
Loading

0 comments on commit a4e71ee

Please sign in to comment.