diff --git a/.vscode/settings.json b/.vscode/settings.json index a70ddf9..2419ef5 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -3,5 +3,8 @@ "." ], "python.testing.unittestEnabled": false, - "python.testing.pytestEnabled": true + "python.testing.pytestEnabled": true, + "[python]": { + "editor.defaultFormatter": "ms-python.black-formatter" + } } diff --git a/database_setup_tools/__init__.py b/database_setup_tools/__init__.py index 2e41e56..47fd1ea 100644 --- a/database_setup_tools/__init__.py +++ b/database_setup_tools/__init__.py @@ -1,4 +1,4 @@ -__version__='1.0.1' +__version__ = "1.0.1" from .session_manager import SessionManager from .setup import DatabaseSetup diff --git a/database_setup_tools/session_manager.py b/database_setup_tools/session_manager.py index e60647a..903bfa7 100644 --- a/database_setup_tools/session_manager.py +++ b/database_setup_tools/session_manager.py @@ -9,7 +9,8 @@ class SessionManager: - """ Manages engines, sessions and connection pools. Thread-safe singleton """ + """Manages engines, sessions and connection pools. Thread-safe singleton""" + _instances = [] _lock = threading.Lock() @@ -21,7 +22,7 @@ def __new__(cls, *args, **kwargs): return cls._get_cached_instance(args, kwargs) def __init__(self, database_uri: str, **kwargs): - """ Session Manager constructor + """Session Manager constructor Args: database_uri (str): The URI of the database to manage sessions for @@ -44,26 +45,26 @@ def __init__(self, database_uri: str, **kwargs): @cached_property def database_uri(self) -> str: - """ Getter for the database URI """ + """Getter for the database URI""" return self._database_uri @property def engine(self) -> Engine: - """ Getter for the engine """ + """Getter for the engine""" return self._engine def get_session(self) -> Generator[Session, None, None]: - """ Provides a (thread safe) scoped session that is wrapped in a context manager """ + """Provides a (thread safe) scoped session that is wrapped in a context manager""" with self._Session() as session: yield session def _get_engine(self, **kwargs) -> Engine: - """ Provides a database engine """ + """Provides a database engine""" return create_engine(self.database_uri, **kwargs) @classmethod def _get_cached_instance(cls, args: tuple, kwargs: dict) -> Optional[object]: - """ Provides a cached instance of the SessionManager class if existing """ + """Provides a cached instance of the SessionManager class if existing""" for instance, arguments in cls._instances: if arguments == (args, kwargs): return instance diff --git a/database_setup_tools/setup.py b/database_setup_tools/setup.py index 20f3cdf..1f602da 100644 --- a/database_setup_tools/setup.py +++ b/database_setup_tools/setup.py @@ -8,7 +8,8 @@ class DatabaseSetup: - """ Create the database and the tables if not done yet """ + """Create the database and the tables if not done yet""" + _instances = [] _lock = threading.Lock() @@ -20,7 +21,7 @@ def __new__(cls, *args, **kwargs): return cls._get_cached_instance(args, kwargs) def __init__(self, model_metadata: MetaData, database_uri: str): - """ Set up a database based on its URI and metadata. Will not overwrite existing data. + """Set up a database based on its URI and metadata. Will not overwrite existing data. Args: model_metadata (Metadata): The metadata of the models to create the tables for @@ -39,7 +40,7 @@ def __init__(self, model_metadata: MetaData, database_uri: str): @property def model_metadata(self) -> MetaData: - """ Getter for the model metadata + """Getter for the model metadata Returns: MetaData: The model metadata @@ -48,7 +49,7 @@ def model_metadata(self) -> MetaData: @property def database_uri(self) -> str: - """ Getter for the database URI + """Getter for the database URI Returns: str: The database URI @@ -56,7 +57,7 @@ def database_uri(self) -> str: return self._database_uri def drop_database(self) -> bool: - """ Drop the database and the tables if possible + """Drop the database and the tables if possible Returns: bool: True if the database was dropped, False otherwise @@ -67,7 +68,7 @@ def drop_database(self) -> bool: return False def create_database(self) -> bool: - """ Create the database and the tables if not done yet """ + """Create the database and the tables if not done yet""" if not sqlalchemy_utils.database_exists(self.database_uri): sqlalchemy_utils.create_database(self.database_uri) session_manager = SessionManager(self.database_uri) @@ -77,7 +78,7 @@ def create_database(self) -> bool: @classmethod def _get_cached_instance(cls, args: tuple, kwargs: dict) -> Optional[object]: - """ Provides a cached instance of the SessionManager class if existing """ + """Provides a cached instance of the SessionManager class if existing""" for instance, arguments in cls._instances: if arguments == (args, kwargs): return instance diff --git a/poetry.lock b/poetry.lock index a4ed2b1..eb69cc0 100644 --- a/poetry.lock +++ b/poetry.lock @@ -40,6 +40,56 @@ docs = ["furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib- tests = ["attrs[tests-no-zope]", "zope.interface"] tests-no-zope = ["cloudpickle", "cloudpickle", "hypothesis", "hypothesis", "mypy (>=0.971,<0.990)", "mypy (>=0.971,<0.990)", "pympler", "pympler", "pytest (>=4.3.0)", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-mypy-plugins", "pytest-xdist[psutil]", "pytest-xdist[psutil]"] +[[package]] +name = "black" +version = "23.1.0" +description = "The uncompromising code formatter." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "black-23.1.0-cp310-cp310-macosx_10_16_arm64.whl", hash = "sha256:b6a92a41ee34b883b359998f0c8e6eb8e99803aa8bf3123bf2b2e6fec505a221"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_universal2.whl", hash = "sha256:57c18c5165c1dbe291d5306e53fb3988122890e57bd9b3dcb75f967f13411a26"}, + {file = "black-23.1.0-cp310-cp310-macosx_10_16_x86_64.whl", hash = "sha256:9880d7d419bb7e709b37e28deb5e68a49227713b623c72b2b931028ea65f619b"}, + {file = "black-23.1.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e6663f91b6feca5d06f2ccd49a10f254f9298cc1f7f49c46e498a0771b507104"}, + {file = "black-23.1.0-cp310-cp310-win_amd64.whl", hash = "sha256:9afd3f493666a0cd8f8df9a0200c6359ac53940cbde049dcb1a7eb6ee2dd7074"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_arm64.whl", hash = "sha256:bfffba28dc52a58f04492181392ee380e95262af14ee01d4bc7bb1b1c6ca8d27"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_universal2.whl", hash = "sha256:c1c476bc7b7d021321e7d93dc2cbd78ce103b84d5a4cf97ed535fbc0d6660648"}, + {file = "black-23.1.0-cp311-cp311-macosx_10_16_x86_64.whl", hash = "sha256:382998821f58e5c8238d3166c492139573325287820963d2f7de4d518bd76958"}, + {file = "black-23.1.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2bf649fda611c8550ca9d7592b69f0637218c2369b7744694c5e4902873b2f3a"}, + {file = "black-23.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:121ca7f10b4a01fd99951234abdbd97728e1240be89fde18480ffac16503d481"}, + {file = "black-23.1.0-cp37-cp37m-macosx_10_16_x86_64.whl", hash = "sha256:a8471939da5e824b891b25751955be52ee7f8a30a916d570a5ba8e0f2eb2ecad"}, + {file = "black-23.1.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8178318cb74f98bc571eef19068f6ab5613b3e59d4f47771582f04e175570ed8"}, + {file = "black-23.1.0-cp37-cp37m-win_amd64.whl", hash = "sha256:a436e7881d33acaf2536c46a454bb964a50eff59b21b51c6ccf5a40601fbef24"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_arm64.whl", hash = "sha256:a59db0a2094d2259c554676403fa2fac3473ccf1354c1c63eccf7ae65aac8ab6"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_universal2.whl", hash = "sha256:0052dba51dec07ed029ed61b18183942043e00008ec65d5028814afaab9a22fd"}, + {file = "black-23.1.0-cp38-cp38-macosx_10_16_x86_64.whl", hash = "sha256:49f7b39e30f326a34b5c9a4213213a6b221d7ae9d58ec70df1c4a307cf2a1580"}, + {file = "black-23.1.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:162e37d49e93bd6eb6f1afc3e17a3d23a823042530c37c3c42eeeaf026f38468"}, + {file = "black-23.1.0-cp38-cp38-win_amd64.whl", hash = "sha256:8b70eb40a78dfac24842458476135f9b99ab952dd3f2dab738c1881a9b38b753"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_arm64.whl", hash = "sha256:a29650759a6a0944e7cca036674655c2f0f63806ddecc45ed40b7b8aa314b651"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_universal2.whl", hash = "sha256:bb460c8561c8c1bec7824ecbc3ce085eb50005883a6203dcfb0122e95797ee06"}, + {file = "black-23.1.0-cp39-cp39-macosx_10_16_x86_64.whl", hash = "sha256:c91dfc2c2a4e50df0026f88d2215e166616e0c80e86004d0003ece0488db2739"}, + {file = "black-23.1.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2a951cc83ab535d248c89f300eccbd625e80ab880fbcfb5ac8afb5f01a258ac9"}, + {file = "black-23.1.0-cp39-cp39-win_amd64.whl", hash = "sha256:0680d4380db3719ebcfb2613f34e86c8e6d15ffeabcf8ec59355c5e7b85bb555"}, + {file = "black-23.1.0-py3-none-any.whl", hash = "sha256:7a0f701d314cfa0896b9001df70a530eb2472babb76086344e688829efd97d32"}, + {file = "black-23.1.0.tar.gz", hash = "sha256:b0bd97bea8903f5a2ba7219257a44e3f1f9d00073d6cc1add68f0beec69692ac"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=3.10.0.0", markers = "python_version < \"3.10\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + [[package]] name = "certifi" version = "2022.12.7" @@ -52,6 +102,21 @@ files = [ {file = "certifi-2022.12.7.tar.gz", hash = "sha256:35824b4c3a97115964b408844d64aa14db1cc518f6562e8d7261699d1350a9e3"}, ] +[[package]] +name = "click" +version = "8.1.3" +description = "Composable command line interface toolkit" +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, + {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + [[package]] name = "colorama" version = "0.4.6" @@ -314,6 +379,18 @@ files = [ {file = "mockito-1.4.0.tar.gz", hash = "sha256:409ab604c9ebe1bb7dc18ec6b0ed98a8ad5127b08273f5804b22f4d1b51e5222"}, ] +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +category = "dev" +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + [[package]] name = "packaging" version = "23.0" @@ -326,6 +403,34 @@ files = [ {file = "packaging-23.0.tar.gz", hash = "sha256:b6ad297f8907de0fa2fe1ccbd26fdaf387f5f47c7275fedf8cce89f99446cf97"}, ] +[[package]] +name = "pathspec" +version = "0.11.1" +description = "Utility library for gitignore style pattern matching of file paths." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pathspec-0.11.1-py3-none-any.whl", hash = "sha256:d8af70af76652554bd134c22b3e8a1cc46ed7d91edcdd721ef1a0c51a84a5293"}, + {file = "pathspec-0.11.1.tar.gz", hash = "sha256:2798de800fa92780e33acca925945e9a19a133b715067cf165b8866c15a31687"}, +] + +[[package]] +name = "platformdirs" +version = "3.2.0" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." +category = "dev" +optional = false +python-versions = ">=3.7" +files = [ + {file = "platformdirs-3.2.0-py3-none-any.whl", hash = "sha256:ebe11c0d7a805086e99506aa331612429a72ca7cd52a1f0d277dc4adc20cb10e"}, + {file = "platformdirs-3.2.0.tar.gz", hash = "sha256:d5b638ca397f25f979350ff789db335903d7ea010ab28903f57b27e1b16c2b08"}, +] + +[package.extras] +docs = ["furo (>=2022.12.7)", "proselint (>=0.13)", "sphinx (>=6.1.3)", "sphinx-autodoc-typehints (>=1.22,!=1.23.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.2.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] + [[package]] name = "pluggy" version = "1.0.0" @@ -718,4 +823,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.9" -content-hash = "535594047b88cfe7b73b84264c32100936f51af1e47688b23631525d07912142" +content-hash = "d9719cd13e8f1a8a09fcc3533b48dc906ac80cfa1e00d2ed2a5c3c56d52f09e2" diff --git a/pyproject.toml b/pyproject.toml index 8bae030..f5cde88 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ sqlalchemy = "^1.4.41" sqlalchemy-utils = "^0.38.3" [tool.poetry.dev-dependencies] +black = "23.1.0" sqlmodel = "0.0.8" psycopg2-binary = "^2.9.5" pytest-cov = "^4.0.0" @@ -27,3 +28,7 @@ build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] addopts = "-x -p no:warnings --cov-report=term --cov-report=term-missing --no-cov-on-fail --cov=database_setup_tools" + +[tool.black] +line-length = 180 +target-version = ['py311'] diff --git a/tests/integration/test_database_integration.py b/tests/integration/test_database_integration.py index 4db1b26..484662c 100644 --- a/tests/integration/test_database_integration.py +++ b/tests/integration/test_database_integration.py @@ -10,9 +10,8 @@ from tests.sample_model import User, model_metadata -@pytest.mark.parametrize('database_uri', DATABASE_URIS) +@pytest.mark.parametrize("database_uri", DATABASE_URIS) class TestDatabaseIntegration: - @pytest.fixture def database_setup(self, database_uri: str) -> DatabaseSetup: setup = DatabaseSetup(model_metadata=model_metadata, database_uri=database_uri) @@ -21,28 +20,28 @@ def database_setup(self, database_uri: str) -> DatabaseSetup: @pytest.fixture def database_session(self, database_uri: str) -> Iterator[ScopedSession]: - """ Get a database session """ + """Get a database session""" session_manager = SessionManager(database_uri) return next(session_manager.get_session()) def test_create_database_and_tables(self, database_setup: DatabaseSetup, database_session: ScopedSession): - """ Test that the tables are created correctly """ + """Test that the tables are created correctly""" # noinspection SqlInjection,SqlDialectInspection - database_session.execute(f'SELECT * FROM {User.__tablename__}') + database_session.execute(f"SELECT * FROM {User.__tablename__}") def test_create_database_multiple_times(self, database_setup: DatabaseSetup, database_session: ScopedSession): - """ Test that creating the database multiple times does not cause problems """ + """Test that creating the database multiple times does not cause problems""" database_setup.create_database() # noinspection SqlInjection,SqlDialectInspection - database_session.execute(f'SELECT * FROM {User.__tablename__}') + database_session.execute(f"SELECT * FROM {User.__tablename__}") def test_drop_database(self, database_setup: DatabaseSetup, database_session: ScopedSession): - """ Test that the database is dropped correctly """ + """Test that the database is dropped correctly""" database_setup.create_database() assert database_setup.drop_database() is True with pytest.raises(OperationalError): # noinspection SqlDialectInspection - database_session.execute(f'SELECT * FROM {User.__tablename__}') + database_session.execute(f"SELECT * FROM {User.__tablename__}") assert database_setup.drop_database() is False diff --git a/tests/sample_model.py b/tests/sample_model.py index feeb0b8..4ab3614 100644 --- a/tests/sample_model.py +++ b/tests/sample_model.py @@ -2,7 +2,7 @@ class User(SQLModel, table=True): - """ User model """ + """User model""" id: int = Field(index=True, primary_key=True) name: str diff --git a/tests/unit/test_session_manager.py b/tests/unit/test_session_manager.py index 6144a86..8df9156 100644 --- a/tests/unit/test_session_manager.py +++ b/tests/unit/test_session_manager.py @@ -8,16 +8,15 @@ class TestSessionManager: - @pytest.fixture def database_uri(self) -> str: - return 'sqlite://' + return "sqlite://" @pytest.fixture def session_manager(self, database_uri: str) -> SessionManager: yield SessionManager(database_uri=database_uri) - @pytest.mark.parametrize('invalid_database_uri', [None, (), 42, False]) + @pytest.mark.parametrize("invalid_database_uri", [None, (), 42, False]) def test_create_session_manager_fail_invalid_database_uri_type(self, invalid_database_uri: Any): with pytest.raises(TypeError): SessionManager(database_uri=invalid_database_uri) @@ -27,16 +26,13 @@ def test_session_manager_is_singleton_with_same_arguments(self, database_uri: st assert SessionManager(database_uri=database_uri) is SessionManager(database_uri=database_uri) def test_session_manager_singletons_with_different_arguments(self): - database_uri_1, database_uri_2 = 'sqlite://', 'postgresql://' + database_uri_1, database_uri_2 = "sqlite://", "postgresql://" assert SessionManager(database_uri=database_uri_1) is not SessionManager(database_uri=database_uri_2) def test_database_uri(self, session_manager: SessionManager, database_uri: str): assert session_manager.database_uri == database_uri - @pytest.mark.parametrize('database_uri, name, driver', [ - ('sqlite://', 'sqlite', 'pysqlite'), - ('postgresql+psycopg2://', 'postgresql', 'psycopg2') - ]) + @pytest.mark.parametrize("database_uri, name, driver", [("sqlite://", "sqlite", "pysqlite"), ("postgresql+psycopg2://", "postgresql", "psycopg2")]) def test_engine(self, database_uri: str, name: str, driver: str): session_manager = SessionManager(database_uri) assert isinstance(session_manager.engine, Engine) diff --git a/tests/unit/test_setup.py b/tests/unit/test_setup.py index 93fe61c..c113ac9 100644 --- a/tests/unit/test_setup.py +++ b/tests/unit/test_setup.py @@ -10,16 +10,13 @@ class TestSetup: - @pytest.fixture def database_uri(self) -> str: - return 'sqlite://' + return "sqlite://" @pytest.fixture def database_setup(self, when: Callable, database_uri: str) -> DatabaseSetup: - when(DatabaseSetup) \ - .create_database() \ - .thenReturn(None) + when(DatabaseSetup).create_database().thenReturn(None) yield DatabaseSetup(model_metadata=model_metadata, database_uri=database_uri) @@ -28,20 +25,20 @@ def test_database_setup_is_singleton_with_same_arguments(self, database_setup: D assert database_setup is DatabaseSetup(model_metadata=model_metadata, database_uri=database_uri) def test_database_setup_is_singleton_with_different_arguments(self, database_setup: DatabaseSetup): - database_uri_1, database_uri_2 = 'sqlite://', 'postgresql://' + database_uri_1, database_uri_2 = "sqlite://", "postgresql://" assert DatabaseSetup(model_metadata=model_metadata, database_uri=database_uri_1) is not DatabaseSetup(model_metadata=model_metadata, database_uri=database_uri_2) def test_create_database_setup_success(self, expect: Callable, database_uri: str): expect(DatabaseSetup, times=1).create_database() DatabaseSetup(model_metadata=model_metadata, database_uri=database_uri) - @pytest.mark.parametrize('invalid_metadata', [None, 'metadata', 42, False]) + @pytest.mark.parametrize("invalid_metadata", [None, "metadata", 42, False]) def test_create_database_setup_fail_modelmetadata_invalid_type(self, invalid_metadata: Any, database_uri: str): with pytest.raises(TypeError): DatabaseSetup(model_metadata=invalid_metadata, database_uri=database_uri) @staticmethod - @pytest.mark.parametrize('invalid_database_uri', [None, model_metadata, 42, False]) + @pytest.mark.parametrize("invalid_database_uri", [None, model_metadata, 42, False]) def test_create_database_setup_fail_database_uri_invalid_type(invalid_database_uri: Any): with pytest.raises(TypeError): DatabaseSetup(model_metadata=model_metadata, database_uri=invalid_database_uri)