From 963f5444734552d4d6f5f225b9aa26f44f2c8dbe Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Fri, 25 Mar 2022 13:46:53 +0100 Subject: [PATCH 01/12] feat: extend resource show output --- karp/cliapp/subapps/resource_subapp.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/karp/cliapp/subapps/resource_subapp.py b/karp/cliapp/subapps/resource_subapp.py index 557a8ab5..a5008699 100644 --- a/karp/cliapp/subapps/resource_subapp.py +++ b/karp/cliapp/subapps/resource_subapp.py @@ -222,14 +222,11 @@ def show( raise typer.Exit(3) typer.echo( - """ - Resource: {resource.resource_id} - EntityId: {resource.entity_id} - Version: {resource.version} - Discarded: {resource.discarded} - Config: {resource.config} - """.format( - resource=resource + tabulate( + ( + (key, value) + for key, value in resource.dict().items() + ) ) ) From bf49a1eb2f80348b26bbbfdf1776561dc7191f03 Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Mon, 28 Mar 2022 09:04:30 +0200 Subject: [PATCH 02/12] stub: add delete entry-repo --- karp/cliapp/subapps/entry_repo_subapp.py | 18 ++++++++++++++++-- .../tests/unit/lex/test_entry_repo_handlers.py | 13 +++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/karp/cliapp/subapps/entry_repo_subapp.py b/karp/cliapp/subapps/entry_repo_subapp.py index ead63096..ce5cde19 100644 --- a/karp/cliapp/subapps/entry_repo_subapp.py +++ b/karp/cliapp/subapps/entry_repo_subapp.py @@ -15,13 +15,11 @@ @subapp.command() def create(infile: typer.FileBinaryRead, ctx: typer.Context): - typer.echo(infile.name) try: data = json.load(infile) except Exception as err: typer.echo(f"Error reading file '{infile.name}': {str(err)}") raise typer.Exit(123) - typer.echo('after json.load') create_entry_repo = CreateEntryRepository.from_dict( data, user='local admin', @@ -39,6 +37,22 @@ def create(infile: typer.FileBinaryRead, ctx: typer.Context): ) +@subapp.command() +def delete( + entity_id: UniqueId, + ctx: typer.Context, + user: Optional[str] = typer.Option(None), +): + + bus = inject_from_ctx(CommandBus, ctx) + + delete_entry_repo = DeleteEntryRepo( + entity_id=entity_id, + user=user or "local admin" + ) + typer.echo(f"Entry repository with id '{entity_id}' deleted.") + + @subapp.command() def list(ctx: typer.Context): query = inject_from_ctx(ListEntryRepos, ctx) diff --git a/karp/tests/unit/lex/test_entry_repo_handlers.py b/karp/tests/unit/lex/test_entry_repo_handlers.py index 1ca4ba73..1c9671e5 100644 --- a/karp/tests/unit/lex/test_entry_repo_handlers.py +++ b/karp/tests/unit/lex/test_entry_repo_handlers.py @@ -18,3 +18,16 @@ def test_create_entry_repository( EntryUowRepositoryUnitOfWork) assert entry_uow_repo_uow.was_committed assert len(entry_uow_repo_uow.repo) == 1 + + +class TestDeleteEntryRepository: + def test_delete_entry_repository_succeeds( + self, + lex_ctx: adapters.UnitTestContext, + ): + cmd = factories.CreateEntryRepositoryFactory() + lex_ctx.command_bus.dispatch(cmd) + + entry_uow_repo_uow = lex_ctx.container.get( + EntryUowRepositoryUnitOfWork) + assert entry_uow_repo_uow.was_committed From 83cd2f254933c5b28cd8e01728124febd9eda97f Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Fri, 25 Mar 2022 22:21:04 +0100 Subject: [PATCH 03/12] test: add test for sql_entry_uow_creator --- .../tests/integration/test_sql_entries_uow.py | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 karp/tests/integration/test_sql_entries_uow.py diff --git a/karp/tests/integration/test_sql_entries_uow.py b/karp/tests/integration/test_sql_entries_uow.py new file mode 100644 index 00000000..3fb509fe --- /dev/null +++ b/karp/tests/integration/test_sql_entries_uow.py @@ -0,0 +1,40 @@ +from unittest import mock + +import pytest + +from karp.foundation.events import EventBus +from karp import lex +from karp.lex_infrastructure import SqlEntryUowCreator +from karp.tests.unit.lex import factories + + +@pytest.fixture +def example_uow() -> lex.CreateEntryRepository: + return factories.CreateEntryRepositoryFactory() + + +@pytest.fixture +def sql_entry_uow_v1_creator(sqlite_session_factory) -> SqlEntryUowCreator: + return SqlEntryUowCreator( + event_bus=mock.Mock(spec=EventBus), + session_factory=sqlite_session_factory, + ) + + +class TestSqlEntryUowV1: + def test_creator_repository_type( + self, + sql_entry_uow_v1_creator: SqlEntryUowCreator, + ): + assert sql_entry_uow_v1_creator.repository_type == 'sql_entries_v1' + + def test_uow_repository_type( + self, + sql_entry_uow_v1_creator: SqlEntryUowCreator, + example_uow: lex.CreateEntryRepository, + ): + entry_uow = sql_entry_uow_v1_creator( + **example_uow.dict(exclude={'repository_type'}) + ) + assert entry_uow.repository_type == 'sql_entries_v1' + From 074d3c019c1321d5407c58e619b0f8fea2a13fbe Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Fri, 25 Mar 2022 22:24:40 +0100 Subject: [PATCH 04/12] test: add test for tablename --- karp/tests/integration/test_sql_entries_uow.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/karp/tests/integration/test_sql_entries_uow.py b/karp/tests/integration/test_sql_entries_uow.py index 3fb509fe..2dc5853a 100644 --- a/karp/tests/integration/test_sql_entries_uow.py +++ b/karp/tests/integration/test_sql_entries_uow.py @@ -38,3 +38,14 @@ def test_uow_repository_type( ) assert entry_uow.repository_type == 'sql_entries_v1' + def test_repo_table_name( + self, + sql_entry_uow_v1_creator: SqlEntryUowCreator, + example_uow: lex.CreateEntryRepository, + ): + entry_uow = sql_entry_uow_v1_creator( + **example_uow.dict(exclude={'repository_type'}) + ) + with entry_uow as uw: + assert uw.repo.history_model.__tablename__ == example_uow.name + From 4993cc43f8d2c4ee31f2f0dd5f777fdbc572b60e Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Fri, 25 Mar 2022 22:28:08 +0100 Subject: [PATCH 05/12] test: add test for tablename --- .../tests/integration/test_sql_entries_uow.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/karp/tests/integration/test_sql_entries_uow.py b/karp/tests/integration/test_sql_entries_uow.py index 2dc5853a..93ea3a7d 100644 --- a/karp/tests/integration/test_sql_entries_uow.py +++ b/karp/tests/integration/test_sql_entries_uow.py @@ -49,3 +49,31 @@ def test_repo_table_name( with entry_uow as uw: assert uw.repo.history_model.__tablename__ == example_uow.name + +class TestSqlEntryUowV2: + def test_creator_repository_type( + self, + sql_entry_uow_v2_creator: SqlEntryUowCreator, + ): + assert sql_entry_uow_v2_creator.repository_type == 'sql_entries_v2' + + def test_uow_repository_type( + self, + sql_entry_uow_v2_creator: SqlEntryUowCreator, + example_uow: lex.CreateEntryRepository, + ): + entry_uow = sql_entry_uow_v2_creator( + **example_uow.dict(exclude={'repository_type'}) + ) + assert entry_uow.repository_type == 'sql_entries_v2' + + def test_repo_table_name( + self, + sql_entry_uow_v2_creator: SqlEntryUowCreator, + example_uow: lex.CreateEntryRepository, + ): + entry_uow = sql_entry_uow_v2_creator( + **example_uow.dict(exclude={'repository_type'}) + ) + with entry_uow as uw: + assert uw.repo.history_model.__tablename__ == f'{example_uow.name}_{str(example_uow.entity_id)} From 3241123ffba0bb323d3d9c5387cb9a59f2408a5f Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Fri, 25 Mar 2022 22:31:38 +0100 Subject: [PATCH 06/12] test: add test for sql_entries_v2 --- karp/tests/integration/test_sql_entries_uow.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/karp/tests/integration/test_sql_entries_uow.py b/karp/tests/integration/test_sql_entries_uow.py index 93ea3a7d..15f49ad0 100644 --- a/karp/tests/integration/test_sql_entries_uow.py +++ b/karp/tests/integration/test_sql_entries_uow.py @@ -4,7 +4,7 @@ from karp.foundation.events import EventBus from karp import lex -from karp.lex_infrastructure import SqlEntryUowCreator +from karp.lex_infrastructure import SqlEntryUowCreator, SqlEntryUowV2Creator from karp.tests.unit.lex import factories @@ -21,6 +21,14 @@ def sql_entry_uow_v1_creator(sqlite_session_factory) -> SqlEntryUowCreator: ) +@pytest.fixture +def sql_entry_uow_v2_creator(sqlite_session_factory) -> SqlEntryUowV2Creator: + return SqlEntryUowV2Creator( + event_bus=mock.Mock(spec=EventBus), + session_factory=sqlite_session_factory, + ) + + class TestSqlEntryUowV1: def test_creator_repository_type( self, @@ -76,4 +84,4 @@ def test_repo_table_name( **example_uow.dict(exclude={'repository_type'}) ) with entry_uow as uw: - assert uw.repo.history_model.__tablename__ == f'{example_uow.name}_{str(example_uow.entity_id)} + assert uw.repo.history_model.__tablename__ == f'{example_uow.name}_{str(example_uow.entity_id)}' From 148854d02c8fc6315fd1a76a10430640ae45d159 Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Fri, 25 Mar 2022 23:29:05 +0100 Subject: [PATCH 07/12] fix: use entity_id in table_name in sql_entries_v2 --- karp/lex_infrastructure/__init__.py | 8 +-- .../repositories/__init__.py | 2 +- .../repositories/sql_entries.py | 49 ++++++++++++++++--- .../tests/integration/test_sql_entries_uow.py | 20 ++++---- 4 files changed, 58 insertions(+), 21 deletions(-) diff --git a/karp/lex_infrastructure/__init__.py b/karp/lex_infrastructure/__init__.py index 302a2cd3..923ab69d 100644 --- a/karp/lex_infrastructure/__init__.py +++ b/karp/lex_infrastructure/__init__.py @@ -43,7 +43,8 @@ ) from karp.lex_infrastructure.repositories import ( SqlEntryUowRepositoryUnitOfWork, - SqlEntryUowCreator, + SqlEntryUowV1Creator, + SqlEntryUowV2Creator, SqlResourceUnitOfWork, ) @@ -102,8 +103,9 @@ def resources_uow( @injector.multiprovider def entry_uow_creator_map(self) -> Dict[str, EntryUnitOfWorkCreator]: return { - 'default': SqlEntryUowCreator, - SqlEntryUowCreator.repository_type: SqlEntryUowCreator, + 'default': SqlEntryUowV2Creator, + SqlEntryUowV1Creator.repository_type: SqlEntryUowV1Creator, + SqlEntryUowV2Creator.repository_type: SqlEntryUowV2Creator, } diff --git a/karp/lex_infrastructure/repositories/__init__.py b/karp/lex_infrastructure/repositories/__init__.py index 9fca5cfd..09d0d8c5 100644 --- a/karp/lex_infrastructure/repositories/__init__.py +++ b/karp/lex_infrastructure/repositories/__init__.py @@ -1,3 +1,3 @@ from .sql_entry_uows import SqlEntryUowRepository, SqlEntryUowRepositoryUnitOfWork -from .sql_entries import SqlEntryUowCreator +from .sql_entries import SqlEntryUowV1Creator, SqlEntryUowV2Creator from .sql_resources import SqlResourceRepository, SqlResourceUnitOfWork diff --git a/karp/lex_infrastructure/repositories/sql_entries.py b/karp/lex_infrastructure/repositories/sql_entries.py index 35fb9420..2f575d29 100644 --- a/karp/lex_infrastructure/repositories/sql_entries.py +++ b/karp/lex_infrastructure/repositories/sql_entries.py @@ -2,7 +2,7 @@ import inspect import logging import typing -from typing import Dict, List, Optional, Tuple +from typing import Dict, List, Optional, Tuple, Generic, TypeVar from uuid import UUID import injector @@ -564,7 +564,7 @@ class SqlEntryUnitOfWork( SqlUnitOfWork, repositories.EntryUnitOfWork, ): - repository_type: str = 'sql_entries_v1' + repository_type: str = 'sql_entries_base' def __init__( self, @@ -585,12 +585,15 @@ def _begin(self): self._session = self.session_factory() if self._entries is None: self._entries = SqlEntryRepository.from_dict( - name=self.name, + name=self.table_name(), resource_config=self.config, session=self._session ) return self + def table_name(self) -> str: + return self.name + @property def repo(self) -> SqlEntryRepository: if self._entries is None: @@ -606,6 +609,18 @@ def collect_new_events(self) -> typing.Iterable: return super().collect_new_events() else: return [] + + +class SqlEntryUnitOfWorkV1(SqlEntryUnitOfWork): + repository_type: str = 'sql_entries_v1' + + +class SqlEntryUnitOfWorkV2(SqlEntryUnitOfWork): + repository_type: str = 'sql_entries_v2' + + def table_name(self) -> str: + return f"{self.name}_{self.entity_id.hex}" + # ===== Value objects ===== # class SqlEntryRepositorySettings(EntryRepositorySettings): # def __init__(self, *, table_name: str, config: Dict): @@ -623,9 +638,11 @@ def collect_new_events(self) -> typing.Iterable: # runtime_table_name, history_model, settings.config # ) # return SqlEntryRepository(history_model, runtime_model, settings.config) +SqlEntryUowType = TypeVar('SqlEntryUowType', bound=SqlEntryUnitOfWork) -class SqlEntryUowCreator: - repository_type: str = SqlEntryUnitOfWork.repository_type + +class SqlEntryUowCreator(Generic[SqlEntryUowType]): + repository_type: str = "repository_type" @injector.inject def __init__( @@ -646,9 +663,9 @@ def __call__( user: str, message: str, timestamp: float, - ) -> SqlEntryUnitOfWork: + ) -> SqlEntryUowType: if entity_id not in self.cache: - self.cache[entity_id] = SqlEntryUnitOfWork( + self.cache[entity_id] = self._create_uow( entity_id=entity_id, name=name, config=config, @@ -660,3 +677,21 @@ def __call__( event_bus=self.event_bus, ) return self.cache[entity_id] + + +class SqlEntryUowV1Creator( + SqlEntryUowCreator[SqlEntryUnitOfWorkV1] +): + repository_type: str = "sql_entries_v1" + + def _create_uow(self, **kwargs) -> SqlEntryUnitOfWorkV1: + return SqlEntryUnitOfWorkV1(**kwargs) + + +class SqlEntryUowV2Creator( + SqlEntryUowCreator[SqlEntryUnitOfWorkV2] +): + repository_type: str = "sql_entries_v2" + + def _create_uow(self, **kwargs) -> SqlEntryUnitOfWorkV2: + return SqlEntryUnitOfWorkV2(**kwargs) diff --git a/karp/tests/integration/test_sql_entries_uow.py b/karp/tests/integration/test_sql_entries_uow.py index 15f49ad0..591141fd 100644 --- a/karp/tests/integration/test_sql_entries_uow.py +++ b/karp/tests/integration/test_sql_entries_uow.py @@ -4,7 +4,7 @@ from karp.foundation.events import EventBus from karp import lex -from karp.lex_infrastructure import SqlEntryUowCreator, SqlEntryUowV2Creator +from karp.lex_infrastructure import SqlEntryUowV1Creator, SqlEntryUowV2Creator from karp.tests.unit.lex import factories @@ -14,8 +14,8 @@ def example_uow() -> lex.CreateEntryRepository: @pytest.fixture -def sql_entry_uow_v1_creator(sqlite_session_factory) -> SqlEntryUowCreator: - return SqlEntryUowCreator( +def sql_entry_uow_v1_creator(sqlite_session_factory) -> SqlEntryUowV1Creator: + return SqlEntryUowV1Creator( event_bus=mock.Mock(spec=EventBus), session_factory=sqlite_session_factory, ) @@ -32,13 +32,13 @@ def sql_entry_uow_v2_creator(sqlite_session_factory) -> SqlEntryUowV2Creator: class TestSqlEntryUowV1: def test_creator_repository_type( self, - sql_entry_uow_v1_creator: SqlEntryUowCreator, + sql_entry_uow_v1_creator: SqlEntryUowV1Creator, ): assert sql_entry_uow_v1_creator.repository_type == 'sql_entries_v1' def test_uow_repository_type( self, - sql_entry_uow_v1_creator: SqlEntryUowCreator, + sql_entry_uow_v1_creator: SqlEntryUowV1Creator, example_uow: lex.CreateEntryRepository, ): entry_uow = sql_entry_uow_v1_creator( @@ -48,7 +48,7 @@ def test_uow_repository_type( def test_repo_table_name( self, - sql_entry_uow_v1_creator: SqlEntryUowCreator, + sql_entry_uow_v1_creator: SqlEntryUowV1Creator, example_uow: lex.CreateEntryRepository, ): entry_uow = sql_entry_uow_v1_creator( @@ -61,13 +61,13 @@ def test_repo_table_name( class TestSqlEntryUowV2: def test_creator_repository_type( self, - sql_entry_uow_v2_creator: SqlEntryUowCreator, + sql_entry_uow_v2_creator: SqlEntryUowV2Creator, ): assert sql_entry_uow_v2_creator.repository_type == 'sql_entries_v2' def test_uow_repository_type( self, - sql_entry_uow_v2_creator: SqlEntryUowCreator, + sql_entry_uow_v2_creator: SqlEntryUowV2Creator, example_uow: lex.CreateEntryRepository, ): entry_uow = sql_entry_uow_v2_creator( @@ -77,11 +77,11 @@ def test_uow_repository_type( def test_repo_table_name( self, - sql_entry_uow_v2_creator: SqlEntryUowCreator, + sql_entry_uow_v2_creator: SqlEntryUowV2Creator, example_uow: lex.CreateEntryRepository, ): entry_uow = sql_entry_uow_v2_creator( **example_uow.dict(exclude={'repository_type'}) ) with entry_uow as uw: - assert uw.repo.history_model.__tablename__ == f'{example_uow.name}_{str(example_uow.entity_id)}' + assert uw.repo.history_model.__tablename__ == f'{example_uow.name}_{example_uow.entity_id.hex}' From 194e99b651591dbb06bec4180290f3261393b913 Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Mon, 28 Mar 2022 11:06:28 +0200 Subject: [PATCH 08/12] fix: shorten the table name --- karp/lex_infrastructure/repositories/sql_entries.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/karp/lex_infrastructure/repositories/sql_entries.py b/karp/lex_infrastructure/repositories/sql_entries.py index 2f575d29..0a46376b 100644 --- a/karp/lex_infrastructure/repositories/sql_entries.py +++ b/karp/lex_infrastructure/repositories/sql_entries.py @@ -1,16 +1,14 @@ """SQL repositories for entries.""" -import inspect import logging import typing -from typing import Dict, List, Optional, Tuple, Generic, TypeVar -from uuid import UUID +from typing import Dict, List, Optional, Generic, TypeVar import injector import regex import sqlalchemy as sa from sqlalchemy import sql from sqlalchemy.orm import sessionmaker -import logging +import ulid from karp.foundation.value_objects import UniqueId from karp.foundation.events import EventBus @@ -190,7 +188,7 @@ def _save(self, entry: Entry): { 'entry_by_entry_id': entry_by_entry_id, 'entry_by_entity_id': entry_by_entity_id, - 'entry': entry.dict(), + 'entry': entry.dict(), } ) raise RuntimeError(f'entry = {entry.dict()}') @@ -619,7 +617,9 @@ class SqlEntryUnitOfWorkV2(SqlEntryUnitOfWork): repository_type: str = 'sql_entries_v2' def table_name(self) -> str: - return f"{self.name}_{self.entity_id.hex}" + u = ulid.from_uuid(self.entity_id) + random_part = u.randomness().str + return f"{self.name}_{random_part}" # ===== Value objects ===== # class SqlEntryRepositorySettings(EntryRepositorySettings): From d5a82c3c840e76138606a5908f4367a769035da3 Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Mon, 28 Mar 2022 11:48:10 +0200 Subject: [PATCH 09/12] test: fix sql_entries_v2 test --- karp/tests/integration/test_sql_entries_uow.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/karp/tests/integration/test_sql_entries_uow.py b/karp/tests/integration/test_sql_entries_uow.py index 591141fd..33dd96ed 100644 --- a/karp/tests/integration/test_sql_entries_uow.py +++ b/karp/tests/integration/test_sql_entries_uow.py @@ -1,6 +1,7 @@ from unittest import mock import pytest +import ulid from karp.foundation.events import EventBus from karp import lex @@ -83,5 +84,6 @@ def test_repo_table_name( entry_uow = sql_entry_uow_v2_creator( **example_uow.dict(exclude={'repository_type'}) ) + random_part = ulid.from_uuid(entry_uow.entity_id).randomness().str with entry_uow as uw: - assert uw.repo.history_model.__tablename__ == f'{example_uow.name}_{example_uow.entity_id.hex}' + assert uw.repo.history_model.__tablename__ == f'{example_uow.name}_{random_part}' From fa1412c25afbcf4d0a6b13436d94f5fd0d34cda5 Mon Sep 17 00:00:00 2001 From: github-actions Date: Mon, 28 Mar 2022 11:24:05 +0000 Subject: [PATCH 10/12] =?UTF-8?q?=F0=9F=93=9D=20Update=20release=20notes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 122a0c10..5ae210ac 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). ## Latest Changes +* Avoid name clashes in SqlEntryRepository. PR [#194](https://github.com/spraakbanken/karp-backend/pull/194) by [@kod-kristoff](https://github.com/kod-kristoff). * Fix correct fetching from repos. PR [#193](https://github.com/spraakbanken/karp-backend/pull/193) by [@kod-kristoff](https://github.com/kod-kristoff). ## 6.0.17 From 3e24fa3bc3dd40fce0e47efd6a313f0d0ff24730 Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Mon, 28 Mar 2022 15:28:37 +0200 Subject: [PATCH 11/12] fix: add num_entities --- karp/cliapp/subapps/entry_repo_subapp.py | 2 ++ karp/foundation/entity.py | 7 +++--- karp/foundation/repository.py | 4 ++++ karp/lex/__init__.py | 11 ++++++++++ karp/lex/application/repositories/entries.py | 3 +++ karp/lex/application/use_cases/__init__.py | 2 +- .../use_cases/entry_repo_handlers.py | 18 +++++++++++++++ karp/lex/domain/commands/__init__.py | 2 +- .../domain/commands/entry_repo_commands.py | 6 +++++ .../repositories/sql_entries.py | 6 +++++ .../repositories/sql_entry_uows.py | 22 +++++++++++++++++++ .../repositories/sql_resources.py | 21 ++++++++++++++++++ .../application/repositories/indicies.py | 3 +++ karp/tests/foundation/unit/test_entity.py | 17 +------------- karp/tests/unit/lex/adapters.py | 11 +++++++++- karp/tests/unit/lex/factories.py | 9 ++++++++ .../unit/lex/test_entry_repo_handlers.py | 8 +++++-- karp/tests/unit/search/adapters.py | 3 +++ 18 files changed, 130 insertions(+), 25 deletions(-) diff --git a/karp/cliapp/subapps/entry_repo_subapp.py b/karp/cliapp/subapps/entry_repo_subapp.py index ce5cde19..f8b0b328 100644 --- a/karp/cliapp/subapps/entry_repo_subapp.py +++ b/karp/cliapp/subapps/entry_repo_subapp.py @@ -1,9 +1,11 @@ import json +from typing import Optional from tabulate import tabulate import typer from karp import lex +from karp.foundation.value_objects import UniqueId from karp.foundation.commands import CommandBus from karp.lex.application.queries import ListEntryRepos from karp.lex.domain.commands import CreateEntryRepository diff --git a/karp/foundation/entity.py b/karp/foundation/entity.py index f65728bb..2c7d5d48 100644 --- a/karp/foundation/entity.py +++ b/karp/foundation/entity.py @@ -131,10 +131,10 @@ def stamp(self, user, *, timestamp: float = None): self._last_modified_by = user self._last_modified = monotonic_utc_now() if timestamp is None else timestamp - def discard(self, *, user, last_modified: float, timestamp: float = None): + def discard(self, *, user, timestamp: float = None): self._check_not_discarded() - self._validate_last_modified(last_modified) - super.discard() + # self._validate_last_modified(last_modified) + super().discard() self._last_modified_by = user self._last_modified = self._ensure_timestamp(timestamp) @@ -236,4 +236,3 @@ def update( self._last_modified_by = user self._last_modified = self._ensure_timestamp(timestamp) self._increment_version() - diff --git a/karp/foundation/repository.py b/karp/foundation/repository.py index 37c05715..760db194 100644 --- a/karp/foundation/repository.py +++ b/karp/foundation/repository.py @@ -58,3 +58,7 @@ def _by_id( self, id_: Union[uuid.UUID, str], *, version: Optional[int] = None ) -> Optional[EntityType]: raise NotImplementedError() + + @abc.abstractmethod + def num_entities(self) -> int: + ... diff --git a/karp/lex/__init__.py b/karp/lex/__init__.py index 9eea9e6a..1a343401 100644 --- a/karp/lex/__init__.py +++ b/karp/lex/__init__.py @@ -10,6 +10,7 @@ from karp.lex.domain.commands import ( CreateEntryRepository, CreateResource, + DeleteEntryRepository, ) from karp.lex.domain import commands from karp.lex.domain.value_objects import EntrySchema @@ -19,6 +20,7 @@ CreatingEntryRepo, CreatingResource, DeletingEntry, + DeletingEntryRepository, DeletingResource, PublishingResource, UpdatingEntry, @@ -55,6 +57,15 @@ def create_entry_repository( entry_repo_uow=uow, ) + @injector.provider + def deleting_entry_repository( + self, + uow: EntryUowRepositoryUnitOfWork, + ) -> CommandHandler[DeleteEntryRepository]: + return DeletingEntryRepository( + entry_repo_uow=uow, + ) + @injector.provider def create_resource( self, diff --git a/karp/lex/application/repositories/entries.py b/karp/lex/application/repositories/entries.py index 03c1286a..24d4df93 100644 --- a/karp/lex/application/repositories/entries.py +++ b/karp/lex/application/repositories/entries.py @@ -185,3 +185,6 @@ def config(self) -> Dict: @property def message(self) -> str: return self._message + + def discard(self, *, user, timestamp: Optional[float] = None): + return entity.TimestampedEntity.discard(self, user=user, timestamp=timestamp) diff --git a/karp/lex/application/use_cases/__init__.py b/karp/lex/application/use_cases/__init__.py index 17907bda..279eb95c 100644 --- a/karp/lex/application/use_cases/__init__.py +++ b/karp/lex/application/use_cases/__init__.py @@ -4,7 +4,7 @@ DeletingEntry, UpdatingEntry, ) -from .entry_repo_handlers import CreatingEntryRepo +from .entry_repo_handlers import CreatingEntryRepo, DeletingEntryRepository from .resource_handlers import ( CreatingResource, DeletingResource, diff --git a/karp/lex/application/use_cases/entry_repo_handlers.py b/karp/lex/application/use_cases/entry_repo_handlers.py index f606c80d..34ef3f4c 100644 --- a/karp/lex/application/use_cases/entry_repo_handlers.py +++ b/karp/lex/application/use_cases/entry_repo_handlers.py @@ -1,4 +1,5 @@ import logging +from typing import Any from karp.foundation.commands import CommandHandler from karp.lex.domain import commands @@ -37,3 +38,20 @@ def execute( uow.repo.save(entry_repo) uow.commit() return entry_repo + + +class DeletingEntryRepository(CommandHandler[commands.DeleteEntryRepository]): + def __init__( + self, + entry_repo_uow: repositories.EntryUowRepositoryUnitOfWork, + **kwargs, + ): + self._entry_repo_uow = entry_repo_uow + + def execute(self, command: commands.DeleteEntryRepository) -> None: + with self._entry_repo_uow as uow: + entry_repo = uow.repo.get_by_id(command.entity_id) + entry_repo.discard( + user=command.user, timestamp=command.timestamp) + uow.repo.save(entry_repo) + uow.commit() diff --git a/karp/lex/domain/commands/__init__.py b/karp/lex/domain/commands/__init__.py index 8c0e5dcf..8cbc77ff 100644 --- a/karp/lex/domain/commands/__init__.py +++ b/karp/lex/domain/commands/__init__.py @@ -6,6 +6,6 @@ DeleteEntry, UpdateEntry, ) -from .entry_repo_commands import CreateEntryRepository +from .entry_repo_commands import CreateEntryRepository, DeleteEntryRepository from .resource_commands import ( CreateResource, DeleteResource, PublishResource, UpdateResource) diff --git a/karp/lex/domain/commands/entry_repo_commands.py b/karp/lex/domain/commands/entry_repo_commands.py index a203f8b6..51a75bce 100644 --- a/karp/lex/domain/commands/entry_repo_commands.py +++ b/karp/lex/domain/commands/entry_repo_commands.py @@ -33,3 +33,9 @@ def from_dict( user=user, message=message or 'Entry repository created' ) + + +class DeleteEntryRepository(Command): + entity_id: UniqueId + message: str + user: str diff --git a/karp/lex_infrastructure/repositories/sql_entries.py b/karp/lex_infrastructure/repositories/sql_entries.py index 0a46376b..3165a41b 100644 --- a/karp/lex_infrastructure/repositories/sql_entries.py +++ b/karp/lex_infrastructure/repositories/sql_entries.py @@ -387,6 +387,12 @@ def get_total_entries(self) -> int: self.runtime_model.discarded).filter_by(discarded=False) return query.count() + def num_entities(self) -> int: + self._check_has_session() + query = self._session.query( + self.runtime_model.discarded).filter_by(discarded=False) + return query.count() + def by_referenceable(self, filters: Optional[Dict] = None, **kwargs) -> List[Entry]: self._check_has_session() # query = self._session.query(self.runtime_model) diff --git a/karp/lex_infrastructure/repositories/sql_entry_uows.py b/karp/lex_infrastructure/repositories/sql_entry_uows.py index d607280c..53203161 100644 --- a/karp/lex_infrastructure/repositories/sql_entry_uows.py +++ b/karp/lex_infrastructure/repositories/sql_entry_uows.py @@ -1,6 +1,7 @@ import logging from typing import Optional +import sqlalchemy as sa from sqlalchemy import sql from sqlalchemy import orm as sa_orm @@ -53,6 +54,27 @@ def _by_id(self, id_: UniqueId, **kwargs) -> Optional[EntryUnitOfWork]: return self._row_to_entity(row) return None + def num_entities(self) -> int: + self._check_has_session() + subq = ( + self._session.query( + EntryUowModel.entity_id, + sa.func.max(EntryUowModel.last_modified).label("maxdate"), + ) + .group_by(EntryUowModel.entity_id) + .subquery("t2") + ) + query = self._session.query(EntryUowModel).join( + subq, + db.and_( + EntryUowModel.entity_id == subq.c.entity_id, + EntryUowModel.last_modified == subq.c.maxdate, + EntryUowModel.discarded == False, + ), + ) + + return query.count() + def _row_to_entity(self, row_proxy) -> EntryUnitOfWork: return self.entry_uow_factory.create( repository_type=row_proxy.type, diff --git a/karp/lex_infrastructure/repositories/sql_resources.py b/karp/lex_infrastructure/repositories/sql_resources.py index 75e829f7..e651a60b 100644 --- a/karp/lex_infrastructure/repositories/sql_resources.py +++ b/karp/lex_infrastructure/repositories/sql_resources.py @@ -178,6 +178,27 @@ def _get_all_resources(self) -> typing.List[entities.Resource]: if resource_dto is not None ] + def num_entities(self) -> int: + self._check_has_session() + subq = ( + self._session.query( + ResourceModel.entity_id, + sa.func.max(ResourceModel.last_modified).label("maxdate"), + ) + .group_by(ResourceModel.entity_id) + .subquery("t2") + ) + query = self._session.query(ResourceModel).join( + subq, + db.and_( + ResourceModel.entity_id == subq.c.entity_id, + ResourceModel.last_modified == subq.c.maxdate, + ResourceModel.discarded == False, + ), + ) + + return query.count() + def _resource_to_dict(self, resource: Resource) -> typing.Dict: return { "history_id": None, diff --git a/karp/search/application/repositories/indicies.py b/karp/search/application/repositories/indicies.py index 32cfefb9..814db1fd 100644 --- a/karp/search/application/repositories/indicies.py +++ b/karp/search/application/repositories/indicies.py @@ -59,6 +59,9 @@ def _save(self, _notused): def _by_id(self, id) -> None: return None + def num_entities(self) -> int: + raise NotImplementedError("num_entities is not used for indicies") + class IndexUnitOfWork( unit_of_work.UnitOfWork[Index] diff --git a/karp/tests/foundation/unit/test_entity.py b/karp/tests/foundation/unit/test_entity.py index 2b6fc3b4..9c9074a0 100644 --- a/karp/tests/foundation/unit/test_entity.py +++ b/karp/tests/foundation/unit/test_entity.py @@ -9,11 +9,6 @@ class TestVersionedEntity: - def test_discard_w_wrong_version_raises_consistency_error(self): - entity = VersionedEntity("id_v1", 1) - - with pytest.raises(ConsistencyError): - entity.discard(version=2) def test_updated_w_wrong_version_raises_consistency_error(self): entity = VersionedEntity("id_v1", 1) @@ -21,7 +16,6 @@ def test_updated_w_wrong_version_raises_consistency_error(self): with pytest.raises(ConsistencyError): entity.update(version=2) - def test_update_increments_version(self): entity = VersionedEntity("id_v1", 1) @@ -31,13 +25,6 @@ def test_update_increments_version(self): class TestTimestampedEntity: - def test_discard_w_wrong_last_modified_raises_consistency_error(self): - entity = TimestampedEntity("id_v1", 1) - with pytest.raises(ConsistencyError): - entity.discard( - last_modified=2, - user="Test", - ) def test_update_w_wrong_last_modified_raises_consistency_error(self): entity = TimestampedEntity("id_v1", 1) @@ -88,7 +75,6 @@ def test_discard_discards(self): assert entity.last_modified_by != previous_last_modified_by assert entity.version == (previous_version + 1) - def test_discard_w_wrong_version_raises_consistency_error(self): entity = TimestampedVersionedEntity("id_v1", version=1) @@ -127,7 +113,7 @@ def test_update_w_wrong_last_modified_raises_consistency_error(self): version=1, last_modified=2, user="Test" - ) + ) def test_update_updates(self): entity = TimestampedVersionedEntity("id_v1", version=1) @@ -146,4 +132,3 @@ def test_update_updates(self): assert entity.last_modified_by != previous_last_modified_by assert entity.last_modified_by == "Test" assert entity.version == (previous_version + 1) - diff --git a/karp/tests/unit/lex/adapters.py b/karp/tests/unit/lex/adapters.py index d097d9c5..b921eb7f 100644 --- a/karp/tests/unit/lex/adapters.py +++ b/karp/tests/unit/lex/adapters.py @@ -56,6 +56,9 @@ def _get_all_resources(self) -> typing.Iterable[lex_entities.Resource]: def resource_ids(self) -> typing.Iterable[str]: return (res.resource_id for res in self.resources) + def num_entities(self) -> int: + return len(self.resources) + class InMemoryReadResourceRepository(ReadOnlyResourceRepository): def __init__(self, resources: Dict): @@ -143,6 +146,9 @@ def from_dict(cls, _): def all_entries(self) -> typing.Iterable[lex_entities.Entry]: yield from self.entries + def num_entities(self) -> int: + return len(self.entries) + class InMemoryEntryUnitOfWork( InMemoryUnitOfWork, lex_repositories.EntryUnitOfWork @@ -246,7 +252,7 @@ def create_entry_uow2( class InMemoryEntryUowRepository(lex_repositories.EntryUowRepository): def __init__(self) -> None: super().__init__() - self._storage = {} + self._storage: dict[UniqueId, dict] = {} def _save(self, entry_repo): self._storage[entry_repo.id] = entry_repo @@ -257,6 +263,9 @@ def _by_id(self, id_, *, version: Optional[int] = None): def __len__(self): return len(self._storage) + def num_entities(self) -> int: + return len(self._storage) + class InMemoryEntryUowRepositoryUnitOfWork(InMemoryUnitOfWork, lex_repositories.EntryUowRepositoryUnitOfWork): def __init__( diff --git a/karp/tests/unit/lex/factories.py b/karp/tests/unit/lex/factories.py index 14e45844..2c393944 100644 --- a/karp/tests/unit/lex/factories.py +++ b/karp/tests/unit/lex/factories.py @@ -125,6 +125,15 @@ class Meta: user = factory.Faker('email') +class DeleteEntryRepositoryFactory(factory.Factory): + class Meta: + model = commands.DeleteEntryRepository + + entity_id = factory.LazyFunction(make_unique_id) + message = 'entry repository deleted' + user = factory.Faker('email') + + class MachineNameFactory(factory.Factory): class Meta: model = MachineName diff --git a/karp/tests/unit/lex/test_entry_repo_handlers.py b/karp/tests/unit/lex/test_entry_repo_handlers.py index 1c9671e5..14f6f582 100644 --- a/karp/tests/unit/lex/test_entry_repo_handlers.py +++ b/karp/tests/unit/lex/test_entry_repo_handlers.py @@ -17,7 +17,7 @@ def test_create_entry_repository( entry_uow_repo_uow = lex_ctx.container.get( EntryUowRepositoryUnitOfWork) assert entry_uow_repo_uow.was_committed - assert len(entry_uow_repo_uow.repo) == 1 + assert entry_uow_repo_uow.repo.num_entities() == 1 class TestDeleteEntryRepository: @@ -30,4 +30,8 @@ def test_delete_entry_repository_succeeds( entry_uow_repo_uow = lex_ctx.container.get( EntryUowRepositoryUnitOfWork) - assert entry_uow_repo_uow.was_committed + assert entry_uow_repo_uow.repo.num_entities() == 1 + + cmd = factories.DeleteEntryRepositoryFactory(entity_id=cmd.entity_id) + lex_ctx.command_bus.dispatch(cmd) + assert entry_uow_repo_uow.repo.num_entities() == 0 diff --git a/karp/tests/unit/search/adapters.py b/karp/tests/unit/search/adapters.py index c804c9ca..e4cac9d1 100644 --- a/karp/tests/unit/search/adapters.py +++ b/karp/tests/unit/search/adapters.py @@ -6,6 +6,7 @@ from karp.foundation.commands import CommandBus from karp.foundation.events import EventBus from karp.foundation.time import utc_now +from karp.lex.application.repositories import entries from karp.search.application.repositories import IndexUnitOfWork, Index, IndexEntry from karp.tests.foundation.adapters import InMemoryUnitOfWork @@ -64,6 +65,8 @@ def delete_entry( # # def statistics(self, resource_id: str, field: str): # return {} + def num_entities(self) -> int: + return sum(len(entries) for entries in self.indicies.values()) class InMemoryIndexUnitOfWork( From 7bb7e4784a7572e71a9d97041ce9fba8dcc255dd Mon Sep 17 00:00:00 2001 From: Kristoffer Andersson Date: Tue, 29 Mar 2022 07:48:36 +0200 Subject: [PATCH 12/12] test: fix num_entities in InMemoryRepos --- karp/tests/unit/lex/adapters.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/karp/tests/unit/lex/adapters.py b/karp/tests/unit/lex/adapters.py index b921eb7f..fc6307ae 100644 --- a/karp/tests/unit/lex/adapters.py +++ b/karp/tests/unit/lex/adapters.py @@ -41,7 +41,7 @@ def _save(self, resource): def _by_id(self, id_, *, version=None): return self.resources.get(id_) - def _by_resource_id(self, resource_id, *, version=None): + def _by_resource_id(self, resource_id): return next((res for res in self.resources.values() if res.resource_id == resource_id), None) def __len__(self): @@ -57,7 +57,7 @@ def resource_ids(self) -> typing.Iterable[str]: return (res.resource_id for res in self.resources) def num_entities(self) -> int: - return len(self.resources) + return sum( not res.discarded for res in self.resources.values() ) class InMemoryReadResourceRepository(ReadOnlyResourceRepository): @@ -147,7 +147,7 @@ def all_entries(self) -> typing.Iterable[lex_entities.Entry]: yield from self.entries def num_entities(self) -> int: - return len(self.entries) + return sum( not e.discarded for e in self.entries.values() ) class InMemoryEntryUnitOfWork( @@ -264,7 +264,7 @@ def __len__(self): return len(self._storage) def num_entities(self) -> int: - return len(self._storage) + return sum(not er.discarded for er in self._storage.values()) class InMemoryEntryUowRepositoryUnitOfWork(InMemoryUnitOfWork, lex_repositories.EntryUowRepositoryUnitOfWork):