diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..0b75b14 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,21 @@ +--- +name: Continuous Integration + +on: + workflow_dispatch: + push: + pull_request: + branches: + - 'main' + +jobs: + ci: + if: ${{ github.event_name == 'push' || github.event.pull_request.merged == true }} + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Run tests + run: | + ./init.sh + docker compose run --rm app poetry run pytest diff --git a/.github/workflows/scratch.yaml b/.github/workflows/scratch.yaml new file mode 100644 index 0000000..5929ddb --- /dev/null +++ b/.github/workflows/scratch.yaml @@ -0,0 +1,19 @@ +#Scratch workflow placeholder to create new workflows from a branch other then main +name: Scratch + +on: + workflow_dispatch: + inputs: + tag: + description: tag + required: true + +jobs: + get_short_tag: + name: get-short-tag + runs-on: ubuntu-latest + steps: + - name: save short tag to environment + run: echo "short_tag=$(echo ${{ github.event.inputs.tag }} | head -c 8 )" >> $GITHUB_ENV + - name: echo env var + run: echo "short_tag ${short_tag}" diff --git a/.gitignore b/.gitignore index 629147c..a0ba3e8 100644 --- a/.gitignore +++ b/.gitignore @@ -19,8 +19,5 @@ output/ tests/test_storage tests/test_workspaces +features/scratch/* -features/scratch/storage/* -!features/scratch/storage/.keep -features/scratch/workspaces/* -!features/scratch/workspaces/.keep diff --git a/dor/adapters/catalog.py b/dor/adapters/catalog.py index a509b91..b779939 100644 --- a/dor/adapters/catalog.py +++ b/dor/adapters/catalog.py @@ -1,4 +1,5 @@ # from dor.domain.models import Bin +from abc import ABC, abstractmethod from dor.domain import models import uuid @@ -46,7 +47,23 @@ class Bin(Base): # DateTime(timezone=True), server_default=func.now() # ) -class MemoryCatalog: + +class Catalog(ABC): + + @abstractmethod + def add(self, bin: models.Bin): + raise NotImplementedError + + @abstractmethod + def get(self, identifier: str): + raise NotImplementedError + + @abstractmethod + def get_by_alternate_identifier(self, identifier: str): + raise NotImplementedError + + +class MemoryCatalog(Catalog): def __init__(self): self.bins = [] @@ -66,7 +83,7 @@ def get_by_alternate_identifier(self, identifier): return None -class SqlalchemyCatalog: +class SqlalchemyCatalog(Catalog): def __init__(self, session): self.session = session diff --git a/dor/cli/main.py b/dor/cli/main.py index 61714d9..4057383 100644 --- a/dor/cli/main.py +++ b/dor/cli/main.py @@ -1,8 +1,10 @@ import typer import dor.cli.samples as samples +import dor.cli.repo as repo app = typer.Typer() app.add_typer(samples.app, name="samples") +app.add_typer(repo.app, name="repo") if __name__ == "__main__": # pragma: no cover diff --git a/dor/cli/repo.py b/dor/cli/repo.py new file mode 100644 index 0000000..2a92df6 --- /dev/null +++ b/dor/cli/repo.py @@ -0,0 +1,85 @@ +import uuid +from typing import Callable, Type, Tuple + +import typer +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker + +from dor.adapters.bag_adapter import BagAdapter +from dor.adapters.catalog import Base, _custom_json_serializer +from dor.config import config +from dor.domain.events import ( + Event, + BinCataloged, + PackageReceived, + PackageStored, + PackageSubmitted, + PackageUnpacked, + PackageVerified, +) +from dor.providers.file_system_file_provider import FilesystemFileProvider +from dor.providers.package_resource_provider import PackageResourceProvider +from dor.providers.translocator import Translocator, Workspace +from dor.service_layer.handlers.catalog_bin import catalog_bin +from dor.service_layer.handlers.receive_package import receive_package +from dor.service_layer.handlers.store_files import store_files +from dor.service_layer.handlers.unpack_package import unpack_package +from dor.service_layer.handlers.verify_package import verify_package +from dor.service_layer.message_bus.memory_message_bus import MemoryMessageBus +from dor.service_layer.unit_of_work import SqlalchemyUnitOfWork +from gateway.ocfl_repository_gateway import OcflRepositoryGateway + + +app = typer.Typer() + + +def bootstrap() -> Tuple[MemoryMessageBus, SqlalchemyUnitOfWork]: + gateway = OcflRepositoryGateway(storage_path=config.storage_path) + + engine = create_engine( + config.get_database_engine_url(), json_serializer=_custom_json_serializer + ) + session_factory = sessionmaker(bind=engine) + uow = SqlalchemyUnitOfWork(gateway=gateway, session_factory=session_factory) + + translocator = Translocator( + inbox_path=config.inbox_path, + workspaces_path=config.workspaces_path, + minter = lambda: str(uuid.uuid4()) + ) + file_provider = FilesystemFileProvider() + + handlers: dict[Type[Event], list[Callable]] = { + PackageSubmitted: [lambda event: receive_package(event, uow, translocator)], + PackageReceived: [lambda event: verify_package(event, uow, BagAdapter, Workspace)], + PackageVerified: [ + lambda event: unpack_package( + event, uow, BagAdapter, PackageResourceProvider, Workspace, file_provider + ) + ], + PackageUnpacked: [lambda event: store_files(event, uow, Workspace)], + PackageStored: [lambda event: catalog_bin(event, uow)], + BinCataloged: [] + } + message_bus = MemoryMessageBus(handlers) + return (message_bus, uow) + + +@app.command() +def initialize(): + gateway = OcflRepositoryGateway(storage_path=config.storage_path) + gateway.create_repository() + + engine = create_engine( + config.get_database_engine_url(), json_serializer=_custom_json_serializer + ) + Base.metadata.create_all(engine) + + +@app.command() +def store( + package_identifier: str = typer.Option(help="Name of the package directory"), +): + message_bus, uow = bootstrap() + event = PackageSubmitted(package_identifier=package_identifier) + message_bus.handle(event, uow) diff --git a/dor/config.py b/dor/config.py index 3add4cd..18972cd 100644 --- a/dor/config.py +++ b/dor/config.py @@ -1,4 +1,5 @@ import os +from pathlib import Path import sqlalchemy from pydantic.dataclasses import dataclass @@ -15,11 +16,17 @@ class DatabaseConfig: @dataclass class Config: + storage_path: Path + inbox_path: Path + workspaces_path: Path database: DatabaseConfig @classmethod def from_env(cls): return cls( + storage_path=Path(os.getenv("STORAGE_PATH", "")), + inbox_path=Path(os.getenv("INBOX_PATH", "")), + workspaces_path=Path(os.getenv("WORKSPACES_PATH", "")), database=DatabaseConfig( user=os.getenv("POSTGRES_USER", "postgres"), password=os.getenv("POSTGRES_PASSWORD", "postgres"), diff --git a/dor/domain/events.py b/dor/domain/events.py index 8730047..7977afb 100644 --- a/dor/domain/events.py +++ b/dor/domain/events.py @@ -2,6 +2,7 @@ from typing import Any from dor.domain.models import VersionInfo +from dor.providers.models import PackageResource @dataclass @@ -38,7 +39,7 @@ class PackageNotVerified(Event): @dataclass class PackageUnpacked(Event): identifier: str - resources: list[Any] + resources: list[PackageResource] tracking_identifier: str version_info: VersionInfo workspace_identifier: str @@ -49,3 +50,9 @@ class PackageUnpacked(Event): class PackageStored(Event): identifier: str tracking_identifier: str + resources: list[PackageResource] + +@dataclass +class BinCataloged(Event): + identifier: str + tracking_identifier: str diff --git a/dor/service_layer/handlers/catalog_bin.py b/dor/service_layer/handlers/catalog_bin.py new file mode 100644 index 0000000..93e77ca --- /dev/null +++ b/dor/service_layer/handlers/catalog_bin.py @@ -0,0 +1,32 @@ +import json +from pathlib import Path + +from dor.domain.events import PackageStored, BinCataloged +from dor.domain.models import Bin +from dor.service_layer.unit_of_work import AbstractUnitOfWork + +def catalog_bin(event: PackageStored, uow: AbstractUnitOfWork) -> None: + root_resource = [resource for resource in event.resources if resource.type == 'Monograph'][0] + common_metadata_file = [ + metadata_file for metadata_file in root_resource.metadata_files if "common" in metadata_file.ref.locref + ][0] + common_metadata_file_path = Path(common_metadata_file.ref.locref) + object_files = uow.gateway.get_object_files(event.identifier) + matching_object_file = [ + object_file for object_file in object_files if common_metadata_file_path == object_file.logical_path + ][0] + literal_common_metadata_path = matching_object_file.literal_path + common_metadata = json.loads(literal_common_metadata_path.read_text()) + + bin = Bin( + identifier=event.identifier, + alternate_identifiers=[root_resource.alternate_identifier.id], + common_metadata=common_metadata, + package_resources=event.resources + ) + with uow: + uow.catalog.add(bin) + uow.commit() + + uow.add_event(BinCataloged(identifier=event.identifier, tracking_identifier=event.tracking_identifier)) + \ No newline at end of file diff --git a/dor/service_layer/handlers/receive_package.py b/dor/service_layer/handlers/receive_package.py index 370bb7d..0ed9316 100644 --- a/dor/service_layer/handlers/receive_package.py +++ b/dor/service_layer/handlers/receive_package.py @@ -1,9 +1,9 @@ from typing import Any from dor.domain.events import PackageSubmitted, PackageReceived -from dor.service_layer.unit_of_work import UnitOfWork +from dor.service_layer.unit_of_work import AbstractUnitOfWork -def receive_package(event: PackageSubmitted, uow: UnitOfWork, translocator: Any) -> None: +def receive_package(event: PackageSubmitted, uow: AbstractUnitOfWork, translocator: Any) -> None: workspace = translocator.create_workspace_for_package(event.package_identifier) received_event = PackageReceived( diff --git a/dor/service_layer/handlers/store_files.py b/dor/service_layer/handlers/store_files.py index 0184921..4619399 100644 --- a/dor/service_layer/handlers/store_files.py +++ b/dor/service_layer/handlers/store_files.py @@ -1,9 +1,9 @@ from pathlib import Path from dor.domain.events import PackageStored, PackageUnpacked -from dor.service_layer.unit_of_work import UnitOfWork +from dor.service_layer.unit_of_work import AbstractUnitOfWork -def store_files(event: PackageUnpacked, uow: UnitOfWork, workspace_class: type) -> None: +def store_files(event: PackageUnpacked, uow: AbstractUnitOfWork, workspace_class: type) -> None: workspace = workspace_class(event.workspace_identifier, event.identifier) entries: list[Path] = [] @@ -24,6 +24,8 @@ def store_files(event: PackageUnpacked, uow: UnitOfWork, workspace_class: type) ) stored_event = PackageStored( - identifier=event.identifier, tracking_identifier=event.tracking_identifier + identifier=event.identifier, + tracking_identifier=event.tracking_identifier, + resources=event.resources ) uow.add_event(stored_event) diff --git a/dor/service_layer/handlers/unpack_package.py b/dor/service_layer/handlers/unpack_package.py index 5820b1b..56ad472 100644 --- a/dor/service_layer/handlers/unpack_package.py +++ b/dor/service_layer/handlers/unpack_package.py @@ -1,13 +1,13 @@ from dor.domain.events import PackageUnpacked, PackageVerified from dor.domain.models import VersionInfo from dor.providers.file_provider import FileProvider -from dor.service_layer.unit_of_work import UnitOfWork +from dor.service_layer.unit_of_work import AbstractUnitOfWork from gateway.coordinator import Coordinator def unpack_package( event: PackageVerified, - uow: UnitOfWork, + uow: AbstractUnitOfWork, bag_adapter_class: type, package_resource_provider_class: type, workspace_class: type, diff --git a/dor/service_layer/handlers/verify_package.py b/dor/service_layer/handlers/verify_package.py index 1baa008..f606792 100644 --- a/dor/service_layer/handlers/verify_package.py +++ b/dor/service_layer/handlers/verify_package.py @@ -1,11 +1,10 @@ from dor.adapters.bag_adapter import ValidationError from dor.domain.events import PackageNotVerified, PackageReceived, PackageVerified from dor.providers.file_provider import FileProvider -from dor.service_layer.unit_of_work import UnitOfWork - +from dor.service_layer.unit_of_work import AbstractUnitOfWork def verify_package( - event: PackageReceived, uow: UnitOfWork, bag_adapter_class: type, workspace_class: type, file_provider: FileProvider + event: PackageReceived, uow: AbstractUnitOfWork, bag_adapter_class: type, workspace_class: type, file_provider: FileProvider ) -> None: workspace = workspace_class(event.workspace_identifier) diff --git a/dor/service_layer/message_bus/memory_message_bus.py b/dor/service_layer/message_bus/memory_message_bus.py index 1aa7b82..3bd35f5 100644 --- a/dor/service_layer/message_bus/memory_message_bus.py +++ b/dor/service_layer/message_bus/memory_message_bus.py @@ -1,6 +1,6 @@ from typing import Callable, Type from dor.domain.events import Event -from dor.service_layer.unit_of_work import UnitOfWork +from dor.service_layer.unit_of_work import AbstractUnitOfWork class MemoryMessageBus: @@ -13,14 +13,14 @@ def register_event_handler(self, event_type: Type[Event], handler: Callable): self.event_handlers[event_type] = [] self.event_handlers[event_type].append(handler) - def handle(self, message, uow: UnitOfWork): + def handle(self, message, uow: AbstractUnitOfWork): # Handles a message, which must be an event. if isinstance(message, Event): self._handle_event(message, uow) else: raise ValueError(f"Message of type {type(message)} is not a valid Event") - def _handle_event(self, event: Event, uow: UnitOfWork): + def _handle_event(self, event: Event, uow: AbstractUnitOfWork): # Handles an event by executing its registered handlers. if event.__class__ not in self.event_handlers: raise NoHandlerForEventError(f"No handler found for event type {type(event)}") diff --git a/dor/service_layer/unit_of_work.py b/dor/service_layer/unit_of_work.py index e35fc68..5cc2314 100644 --- a/dor/service_layer/unit_of_work.py +++ b/dor/service_layer/unit_of_work.py @@ -3,13 +3,15 @@ from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker -from dor.adapters.catalog import MemoryCatalog, SqlalchemyCatalog, _custom_json_serializer +from dor.adapters.catalog import Catalog, MemoryCatalog, SqlalchemyCatalog, _custom_json_serializer from dor.config import config from dor.domain.events import Event from gateway.repository_gateway import RepositoryGateway class AbstractUnitOfWork(ABC): + catalog: Catalog + gateway: RepositoryGateway @abstractmethod def __enter__(self): diff --git a/env.example b/env.example index 5b3b9aa..dc4db15 100644 --- a/env.example +++ b/env.example @@ -9,3 +9,7 @@ POSTGRES_USER=postgres POSTGRES_PASSWORD=postgres POSTGRES_DATABASE=dor_local POSTGRES_HOST=db + +STORAGE_PATH= +INBOX_PATH= +WORKSPACES_PATH= diff --git a/features/inspect_bin.feature b/features/inspect_bin.feature index 70531f1..881f102 100644 --- a/features/inspect_bin.feature +++ b/features/inspect_bin.feature @@ -11,12 +11,12 @@ Feature: Inspect Bin As a Collection Manager I want to review the contents of its bin. - Scenario: + Scenario: Revision summary Given a preserved monograph with an alternate identifier of "xyzzy:00000001" When the Collection Manager looks up the bin by "xyzzy:00000001" Then the Collection Manager sees the summary of the bin - Scenario: + Scenario: Revision file sets Given a preserved monograph with an alternate identifier of "xyzzy:00000001" When the Collection Manager lists the contents of the bin for "xyzzy:00000001" Then the Collection Manager sees the file sets. diff --git a/features/scratch/workspaces/.keep b/features/scratch/workspaces/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/features/steps/store_resource.py b/features/steps/store_resource.py index 76c31a8..c2440b6 100644 --- a/features/steps/store_resource.py +++ b/features/steps/store_resource.py @@ -262,3 +262,4 @@ def step_impl(context): event = context.stored_event assert event.identifier == "00000000-0000-0000-0000-000000000001" assert context.uow.gateway.has_object(event.identifier) + diff --git a/features/steps/inspect_bin.py b/features/steps/test_inspect_bin.py similarity index 82% rename from features/steps/inspect_bin.py rename to features/steps/test_inspect_bin.py index 5e8586e..6c4ad50 100644 --- a/features/steps/inspect_bin.py +++ b/features/steps/test_inspect_bin.py @@ -1,28 +1,54 @@ -from behave import given, then, when import uuid from datetime import datetime, UTC +from functools import partial +import pytest from pydantic_core import to_jsonable_python +from pytest_bdd import scenario, given, when, then, parsers from sqlalchemy import create_engine from sqlalchemy.orm import sessionmaker from dor.adapters.catalog import Base, _custom_json_serializer from dor.config import config from dor.domain.models import Bin -from dor.service_layer import catalog_service -from dor.service_layer.unit_of_work import SqlalchemyUnitOfWork, UnitOfWork from dor.providers.models import ( Agent, AlternateIdentifier, FileMetadata, FileReference, PackageResource, PreservationEvent, StructMap, StructMapItem, StructMapType ) +from dor.service_layer import catalog_service +from dor.service_layer.unit_of_work import AbstractUnitOfWork, SqlalchemyUnitOfWork from gateway.fake_repository_gateway import FakeRepositoryGateway +scenario = partial(scenario, '../inspect_bin.feature') + +@scenario('Revision summary') +def test_revision_summary(): + pass + +@scenario('Revision file sets') +def test_revision_file_sets(): + pass + +@pytest.fixture +def unit_of_work() -> AbstractUnitOfWork: + engine = create_engine( + config.get_test_database_engine_url(), json_serializer=_custom_json_serializer + ) + session_factory = sessionmaker(bind=engine) + Base.metadata.drop_all(engine) + Base.metadata.create_all(engine) -@given(u'a preserved monograph with an alternate identifier of "{alt_id}"') -def step_impl(context, alt_id): + uow = SqlalchemyUnitOfWork(gateway=FakeRepositoryGateway(), session_factory=session_factory) + return uow + +@given( + parsers.parse(u'a preserved monograph with an alternate identifier of "{alt_id}"'), + target_fixture="bin" +) +def _(alt_id, unit_of_work: AbstractUnitOfWork): bin = Bin( identifier=uuid.UUID("00000000-0000-0000-0000-000000000001"), - alternate_identifiers=["xyzzy:00000001"], + alternate_identifiers=[alt_id], common_metadata={ "@schema": "urn:umich.edu:dor:schema:common", "title": "Discussion also Republican owner hot already itself.", @@ -181,30 +207,27 @@ def step_impl(context, alt_id): ] ) - engine = create_engine( - config.get_test_database_engine_url(), json_serializer=_custom_json_serializer - ) - session_factory = sessionmaker(bind=engine) - Base.metadata.drop_all(engine) - Base.metadata.create_all(engine) + with unit_of_work: + unit_of_work.catalog.add(bin) + unit_of_work.commit() - context.uow = SqlalchemyUnitOfWork(gateway=FakeRepositoryGateway(), session_factory=session_factory) - with context.uow: - context.uow.catalog.add(bin) - context.uow.commit() + return bin -@when(u'the Collection Manager looks up the bin by "{alt_id}"') -def step_impl(context, alt_id): - context.alt_id = alt_id - with context.uow: - context.bin = context.uow.catalog.get_by_alternate_identifier(alt_id) - context.summary = catalog_service.summarize(context.bin) +@when( + parsers.parse(u'the Collection Manager looks up the bin by "{alt_id}"'), + target_fixture="summary" +) +def _(alt_id, unit_of_work: AbstractUnitOfWork): + with unit_of_work: + bin = unit_of_work.catalog.get_by_alternate_identifier(alt_id) + summary = catalog_service.summarize(bin) + return summary @then(u'the Collection Manager sees the summary of the bin') -def step_impl(context): +def _(bin: Bin, summary): expected_summary = dict( identifier="00000000-0000-0000-0000-000000000001", - alternate_identifiers=[context.alt_id], + alternate_identifiers=bin.alternate_identifiers, common_metadata={ "@schema": "urn:umich.edu:dor:schema:common", "title": "Discussion also Republican owner hot already itself.", @@ -216,19 +239,23 @@ def step_impl(context): ] } ) - assert context.summary == expected_summary + assert summary == expected_summary -@when(u'the Collection Manager lists the contents of the bin for "{alt_id}"') -def step_impl(context, alt_id): - with context.uow: - context.bin = context.uow.catalog.get_by_alternate_identifier(alt_id) - context.file_sets = catalog_service.get_file_sets(context.bin) +@when( + parsers.parse(u'the Collection Manager lists the contents of the bin for "{alt_id}"'), + target_fixture="file_sets" +) +def _(alt_id, unit_of_work): + with unit_of_work: + bin = unit_of_work.catalog.get_by_alternate_identifier(alt_id) + file_sets = catalog_service.get_file_sets(bin) + return file_sets @then(u'the Collection Manager sees the file sets.') -def step_impl(context): +def _(bin: Bin, file_sets): expected_file_sets = [ to_jsonable_python(resource) - for resource in context.bin.package_resources if resource.type == 'Asset' + for resource in bin.package_resources if resource.type == 'Asset' ] - assert context.file_sets == expected_file_sets + assert file_sets == expected_file_sets diff --git a/features/steps/test_store_resource.py b/features/steps/test_store_resource.py index ba08d7a..6a89736 100644 --- a/features/steps/test_store_resource.py +++ b/features/steps/test_store_resource.py @@ -1,109 +1,131 @@ from dataclasses import dataclass +from functools import partial from pathlib import Path from typing import Callable, Type -from functools import partial -from dor.providers.file_provider import FileProvider -from dor.providers.file_system_file_provider import FilesystemFileProvider +import pytest + from pytest_bdd import scenario, given, when, then +from sqlalchemy import create_engine +from sqlalchemy.orm import sessionmaker from dor.adapters.bag_adapter import BagAdapter +from dor.adapters.catalog import Base, _custom_json_serializer +from dor.config import config from dor.domain.events import ( Event, + BinCataloged, PackageReceived, PackageStored, PackageSubmitted, PackageVerified, PackageUnpacked ) -from dor.providers.translocator import Translocator, Workspace +from dor.providers.file_system_file_provider import FilesystemFileProvider from dor.providers.package_resource_provider import PackageResourceProvider +from dor.providers.translocator import Translocator, Workspace +from dor.service_layer.handlers.catalog_bin import catalog_bin +from dor.service_layer.handlers.receive_package import receive_package from dor.service_layer.handlers.store_files import store_files +from dor.service_layer.handlers.unpack_package import unpack_package +from dor.service_layer.handlers.verify_package import verify_package from dor.service_layer.message_bus.memory_message_bus import MemoryMessageBus -from dor.service_layer.unit_of_work import UnitOfWork +from dor.service_layer.unit_of_work import AbstractUnitOfWork, SqlalchemyUnitOfWork from gateway.ocfl_repository_gateway import OcflRepositoryGateway -from dor.service_layer.handlers.receive_package import receive_package -from dor.service_layer.handlers.verify_package import verify_package -from dor.service_layer.handlers.unpack_package import unpack_package @dataclass -class Context: - uow: UnitOfWork = None - translocator: Translocator = None - message_bus: MemoryMessageBus = None - stored_event: PackageStored = None - file_provider: FileProvider = FilesystemFileProvider() - - -scenario = partial(scenario, '../store_resource.feature') +class PathData: + scratch: Path + storage: Path + workspaces: Path + inbox: Path + +@pytest.fixture +def path_data() -> PathData: + scratch = Path("./features/scratch") + + return PathData( + scratch=scratch, + inbox=Path("./features/fixtures/inbox"), + workspaces=scratch / "workspaces", + storage=scratch / "storage" + ) -@scenario('Storing a new resource for immediate release') -def test_store_resource(): - pass +@pytest.fixture +def unit_of_work(path_data: PathData) -> AbstractUnitOfWork: + engine = create_engine( + config.get_test_database_engine_url(), json_serializer=_custom_json_serializer + ) + session_factory = sessionmaker(bind=engine) + Base.metadata.drop_all(engine) + Base.metadata.create_all(engine) -@given(u'a package containing the scanned pages, OCR, and metadata', target_fixture="context") -def _(): - context = Context(uow=None, translocator=None, message_bus=None) + gateway = OcflRepositoryGateway(storage_path=path_data.storage) - inbox = Path("./features/fixtures/inbox") - storage = Path("./features/scratch/storage") - workspaces = Path("./features/scratch/workspaces") + return SqlalchemyUnitOfWork(gateway=gateway, session_factory=session_factory) +@pytest.fixture +def message_bus(path_data: PathData, unit_of_work: AbstractUnitOfWork) -> MemoryMessageBus: value = '55ce2f63-c11a-4fac-b3a9-160305b1a0c4' - - context.file_provider.delete_dir_and_contents( - Path(f"./features/scratch/workspaces/{value}") + translocator = Translocator( + inbox_path=path_data.inbox, + workspaces_path=path_data.workspaces, + minter = lambda: value ) - context.file_provider.delete_dir_and_contents(storage) - context.file_provider.create_directory(storage) - - gateway = OcflRepositoryGateway(storage_path = storage) - gateway.create_repository() - context.uow = UnitOfWork(gateway=gateway) - - context.translocator = Translocator(inbox_path = inbox, workspaces_path = workspaces, minter = lambda: value, file_provider=context.file_provider) - - def stored_callback(event: PackageStored, uow: UnitOfWork) -> None: - context.stored_event = event handlers: dict[Type[Event], list[Callable]] = { PackageSubmitted: [ - lambda event: receive_package(event, context.uow, context.translocator) + lambda event: receive_package(event, unit_of_work, translocator) ], PackageReceived: [ lambda event: verify_package( - event, - context.uow, - BagAdapter, - Workspace, - context.file_provider + event, unit_of_work, BagAdapter, Workspace, FilesystemFileProvider() ) ], PackageVerified: [ lambda event: unpack_package( event, - context.uow, + unit_of_work, BagAdapter, PackageResourceProvider, Workspace, - context.file_provider, + FilesystemFileProvider(), ) ], - PackageUnpacked: [lambda event: store_files(event, context.uow, Workspace)], - PackageStored: [lambda event: stored_callback(event, context.uow)], + PackageUnpacked: [lambda event: store_files(event, unit_of_work, Workspace)], + PackageStored: [lambda event: catalog_bin(event, unit_of_work)], + BinCataloged: [], } - context.message_bus = MemoryMessageBus(handlers) - return context + message_bus = MemoryMessageBus(handlers) + return message_bus + +scenario = partial(scenario, '../store_resource.feature') + +@scenario('Storing a new resource for immediate release') +def test_store_resource(): + pass + +@given(u'a package containing the scanned pages, OCR, and metadata') +def _(path_data: PathData, unit_of_work: AbstractUnitOfWork): + shutil.rmtree(path=path_data.scratch, ignore_errors = True) + os.mkdir(path_data.scratch) + os.mkdir(path_data.storage) + os.mkdir(path_data.workspaces) + + unit_of_work.gateway.create_repository() @when(u'the Collection Manager places the packaged resource in the incoming location') -def _(context): +def _(message_bus: MemoryMessageBus, unit_of_work: AbstractUnitOfWork): submission_id = "xyzzy-0001-v1" event = PackageSubmitted(package_identifier=submission_id) - context.message_bus.handle(event, context.uow) + message_bus.handle(event, unit_of_work) @then(u'the Collection Manager can see that it was preserved.') -def _(context): - event = context.stored_event - assert event.identifier == "00000000-0000-0000-0000-000000000001" - assert context.uow.gateway.has_object(event.identifier) +def _(unit_of_work: AbstractUnitOfWork): + expected_identifier = "00000000-0000-0000-0000-000000000001" + assert unit_of_work.gateway.has_object(expected_identifier) + + with unit_of_work: + bin = unit_of_work.catalog.get(expected_identifier) + assert bin is not None diff --git a/poetry.lock b/poetry.lock index b2795fe..62e0ecb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -42,26 +42,6 @@ files = [ {file = "bagit-1.9b2.tar.gz", hash = "sha256:4450cbe591fd3669471fdf5aab30186e47f71ae596b58bf3d6a416182ea5e1bf"}, ] -[[package]] -name = "behave" -version = "1.2.6" -description = "behave is behaviour-driven development, Python style" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -files = [ - {file = "behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"}, - {file = "behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86"}, -] - -[package.dependencies] -parse = ">=1.8.2" -parse-type = ">=0.4.2" -six = ">=1.11" - -[package.extras] -develop = ["coverage", "invoke (>=0.21.0)", "modernize (>=0.5)", "path.py (>=8.1.2)", "pathlib", "pycmd", "pylint", "pytest (>=3.0)", "pytest-cov", "tox"] -docs = ["sphinx (>=1.6)", "sphinx-bootstrap-theme (>=0.6)"] - [[package]] name = "certifi" version = "2024.12.14" @@ -1566,4 +1546,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.12" -content-hash = "54c21a9779a8b8549941c4946450b9efc5a88c55e7d4ec15e0129e4640f2f73a" +content-hash = "f466b41b966b0b0ce6f52545f2545166ca0ca66bdef7af5a58a6eff70248923c" diff --git a/pyproject.toml b/pyproject.toml index 10f9966..e8aa7e9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,7 +28,6 @@ fastapi = {extras = ["standard"], version = "^0.115.6"} [tool.poetry.group.dev.dependencies] pytest = "^8.0.2" ruff = "^0.2.2" -behave = "^1.2.6" pytest-bdd = "^8.1.0" [build-system]