diff --git a/Makefile b/Makefile index aed6a74..451087b 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ test: - pytest + pytest tests/lending tests/catalogue test-cov: - pytest --cov=src/lending --cov-report=term-missing --cov-branch + pytest tests/lending tests/catalogue --cov=src/lending --cov=src/catalogue --cov-report=term-missing --cov-branch diff --git a/README.md b/README.md index 70ddb50..dc890b4 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,8 @@ Protean example implementation |-----------------|-------------------------------------------------------------------------| | Source | [DDD by Examples - Library](https://github.com/ddd-by-examples/library) | | Pattern | CQRS | -| Protean Version | 0.12.0 | +| Protean Version | 0.12.1 | +| Build Status | [Build Status](https://github.com/proteanhq/library-cqrs/actions/workflows/ci.yml/badge.svg) | | Coverage | [![codecov](https://codecov.io/github/proteanhq/library-cqrs/graph/badge.svg?token=onIFcl4Dg5)](https://codecov.io/github/proteanhq/library-cqrs)| ## Contributing diff --git a/docker-compose.yml b/docker-compose.yml index b4bf049..00ccfea 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,13 @@ services: ports: - 5433:5432 + redis: + image: redis:7.0.11 + ports: + - "6379:6379" + environment: + - ALLOW_EMPTY_PASSWORD=yes + volumes: db-data: driver: local diff --git a/poetry.lock b/poetry.lock index 9dabe65..b47bbd9 100644 --- a/poetry.lock +++ b/poetry.lock @@ -49,6 +49,17 @@ six = ">=1.12.0" astroid = ["astroid (>=1,<2)", "astroid (>=2,<4)"] test = ["astroid (>=1,<2)", "astroid (>=2,<4)", "pytest"] +[[package]] +name = "async-timeout" +version = "4.0.3" +description = "Timeout context manager for asyncio programs" +optional = false +python-versions = ">=3.7" +files = [ + {file = "async-timeout-4.0.3.tar.gz", hash = "sha256:4640d96be84d82d02ed59ea2b7105a0f7b33abe8703703cd0ab0bf87c427522f"}, + {file = "async_timeout-4.0.3-py3-none-any.whl", hash = "sha256:7405140ff1230c310e51dc27b3145b9092d659ce68ff733fb0cefe3ee42be028"}, +] + [[package]] name = "bleach" version = "6.1.0" @@ -840,6 +851,20 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "message-db-py" +version = "0.2.0" +description = "The Python interface to the MessageDB Event Store and Message Store" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "message_db_py-0.2.0-py3-none-any.whl", hash = "sha256:8caa5e5c3a4083e7d2a5b9b0bd98074e1e86a5f82a49641376b06f18453e9912"}, + {file = "message_db_py-0.2.0.tar.gz", hash = "sha256:fbeb24c8eb28c55cf080865e8e21478e1e64292aa87491ce4026fd2f71d7b1bc"}, +] + +[package.dependencies] +psycopg2 = ">=2.9.9,<3.0.0" + [[package]] name = "nodeenv" version = "1.9.1" @@ -1030,17 +1055,20 @@ copier = "^9.1.1" inflection = ">=0.5.1" ipython = "^8.23.0" marshmallow = ">=3.15.0" +message-db-py = {version = ">=0.2.0", optional = true} +psycopg2 = {version = ">=2.9.9", optional = true} python-dateutil = ">=2.8.2" +redis = {version = "~5.0.7", optional = true} +sqlalchemy = {version = "~2.0.30", optional = true} typer = ">=0.12.3" werkzeug = ">=2.0.0" [package.extras] -celery = ["celery[redis] (>=5.2.7,<5.3.0)"] elasticsearch = ["elasticsearch (>=7.17.9,<7.18.0)", "elasticsearch-dsl (>=7.4.1,<7.5.0)"] flask = ["flask (>=1.1.1)"] message-db = ["message-db-py (>=0.2.0)"] postgresql = ["psycopg2 (>=2.9.9)", "sqlalchemy (>=2.0.30,<2.1.0)"] -redis = ["redis (>=3.5.2,<3.6.0)"] +redis = ["redis (>=5.0.7,<5.1.0)"] sendgrid = ["sendgrid (>=6.1.3)"] sqlite = ["sqlalchemy (>=2.0.30,<2.1.0)"] @@ -1048,7 +1076,29 @@ sqlite = ["sqlalchemy (>=2.0.30,<2.1.0)"] type = "git" url = "https://github.com/proteanhq/protean.git" reference = "main" -resolved_reference = "36f2c6873a68e2a0c49801ee00664209e938412c" +resolved_reference = "792bf75e18c1c956e943ef7973edfad212bcf816" + +[[package]] +name = "psycopg2" +version = "2.9.9" +description = "psycopg2 - Python-PostgreSQL Database Adapter" +optional = false +python-versions = ">=3.7" +files = [ + {file = "psycopg2-2.9.9-cp310-cp310-win32.whl", hash = "sha256:38a8dcc6856f569068b47de286b472b7c473ac7977243593a288ebce0dc89516"}, + {file = "psycopg2-2.9.9-cp310-cp310-win_amd64.whl", hash = "sha256:426f9f29bde126913a20a96ff8ce7d73fd8a216cfb323b1f04da402d452853c3"}, + {file = "psycopg2-2.9.9-cp311-cp311-win32.whl", hash = "sha256:ade01303ccf7ae12c356a5e10911c9e1c51136003a9a1d92f7aa9d010fb98372"}, + {file = "psycopg2-2.9.9-cp311-cp311-win_amd64.whl", hash = "sha256:121081ea2e76729acfb0673ff33755e8703d45e926e416cb59bae3a86c6a4981"}, + {file = "psycopg2-2.9.9-cp312-cp312-win32.whl", hash = "sha256:d735786acc7dd25815e89cc4ad529a43af779db2e25aa7c626de864127e5a024"}, + {file = "psycopg2-2.9.9-cp312-cp312-win_amd64.whl", hash = "sha256:a7653d00b732afb6fc597e29c50ad28087dcb4fbfb28e86092277a559ae4e693"}, + {file = "psycopg2-2.9.9-cp37-cp37m-win32.whl", hash = "sha256:5e0d98cade4f0e0304d7d6f25bbfbc5bd186e07b38eac65379309c4ca3193efa"}, + {file = "psycopg2-2.9.9-cp37-cp37m-win_amd64.whl", hash = "sha256:7e2dacf8b009a1c1e843b5213a87f7c544b2b042476ed7755be813eaf4e8347a"}, + {file = "psycopg2-2.9.9-cp38-cp38-win32.whl", hash = "sha256:ff432630e510709564c01dafdbe996cb552e0b9f3f065eb89bdce5bd31fabf4c"}, + {file = "psycopg2-2.9.9-cp38-cp38-win_amd64.whl", hash = "sha256:bac58c024c9922c23550af2a581998624d6e02350f4ae9c5f0bc642c633a2d5e"}, + {file = "psycopg2-2.9.9-cp39-cp39-win32.whl", hash = "sha256:c92811b2d4c9b6ea0285942b2e7cac98a59e166d59c588fe5cfe1eda58e72d59"}, + {file = "psycopg2-2.9.9-cp39-cp39-win_amd64.whl", hash = "sha256:de80739447af31525feddeb8effd640782cf5998e1a4e9192ebdf829717e3913"}, + {file = "psycopg2-2.9.9.tar.gz", hash = "sha256:d1454bde93fb1e224166811694d600e746430c006fbb031ea06ecc2ea41bf156"}, +] [[package]] name = "ptyprocess" @@ -1397,6 +1447,24 @@ prompt_toolkit = ">=2.0,<4.0" [package.extras] docs = ["Sphinx (>=3.3,<4.0)", "sphinx-autobuild (>=2020.9.1,<2021.0.0)", "sphinx-autodoc-typehints (>=1.11.1,<2.0.0)", "sphinx-copybutton (>=0.3.1,<0.4.0)", "sphinx-rtd-theme (>=0.5.0,<0.6.0)"] +[[package]] +name = "redis" +version = "5.0.7" +description = "Python client for Redis database and key-value store" +optional = false +python-versions = ">=3.7" +files = [ + {file = "redis-5.0.7-py3-none-any.whl", hash = "sha256:0e479e24da960c690be5d9b96d21f7b918a98c0cf49af3b6fafaa0753f93a0db"}, + {file = "redis-5.0.7.tar.gz", hash = "sha256:8f611490b93c8109b50adc317b31bfd84fff31def3475b92e7e80bf39f48175b"}, +] + +[package.dependencies] +async-timeout = {version = ">=4.0.3", markers = "python_full_version < \"3.11.3\""} + +[package.extras] +hiredis = ["hiredis (>=1.0.0)"] +ocsp = ["cryptography (>=36.0.1)", "pyopenssl (==20.0.1)", "requests (>=2.26.0)"] + [[package]] name = "rich" version = "13.7.1" @@ -1939,4 +2007,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "09ac1550eb95c2aae37e319dfeba8c0e83968b1baa7b45d0d2db87bfb010ee65" +content-hash = "293a1341170a821b3380dc900c8f7425ee9142fe3bc4f4d999c04f8d46684e8c" diff --git a/pyproject.toml b/pyproject.toml index 7762eee..be74728 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,9 +19,11 @@ packages = [ [tool.poetry.dependencies] python = "^3.11" # protean = {version = "0.12.1", extras=[] } -protean = { git = "https://github.com/proteanhq/protean.git", branch = "main" } +protean = { git = "https://github.com/proteanhq/protean.git", branch = "main" , extras=["postgresql", "sqlite", "message_db", "redis"]} +# protean = { path = "../../protean", develop=true} sqlalchemy = "^2.0.31" fastapi = "^0.111.1" +redis = "^5.0.7" [tool.poetry.group.test] optional = true diff --git a/src/catalogue/main.py b/src/catalogue/main.py index 96ec6ad..95362d0 100644 --- a/src/catalogue/main.py +++ b/src/catalogue/main.py @@ -1,5 +1,7 @@ +import json from typing import Annotated +import redis from fastapi import Depends, FastAPI from pydantic import BaseModel, Field from sqlalchemy.orm import Session @@ -14,6 +16,10 @@ Base.metadata.create_all(bind=engine) +# Redis setup +redis_client = redis.Redis(host="localhost", port=6379, db=0) + + def get_db(): db = SessionLocal() try: @@ -56,4 +62,17 @@ async def add_book_instance( db.add(new_book_instance) db.commit() + + # Raise Event + event_dict = { + "instance_id": new_book_instance.id, + "isbn": new_book_instance.isbn, + "title": new_book_instance.book.title, + "summary": new_book_instance.book.summary, + "price": new_book_instance.book.price, + "is_circulating": new_book_instance.is_circulating, + "added_at": new_book_instance.added_at.isoformat(), + } + redis_client.publish("book_instance_added", json.dumps(event_dict)) + return {"message": "Book instance added successfully"} diff --git a/src/catalogue/models.py b/src/catalogue/models.py index 8308e9d..864d14d 100644 --- a/src/catalogue/models.py +++ b/src/catalogue/models.py @@ -1,4 +1,15 @@ -from sqlalchemy import Boolean, Column, Float, ForeignKey, Integer, String, Text +from sqlalchemy import ( + Boolean, + Column, + DateTime, + Float, + ForeignKey, + Integer, + String, + Text, + func, +) +from sqlalchemy.orm import relationship from .database import Base @@ -11,6 +22,8 @@ class Book(Base): summary = Column(Text) price = Column(Float, nullable=False) + instances = relationship("BookInstance", back_populates="book") + class BookInstance(Base): __tablename__ = "book_instances" @@ -18,3 +31,6 @@ class BookInstance(Base): id = Column(Integer, primary_key=True, index=True, autoincrement=True) isbn = Column(String(13), ForeignKey("books.isbn"), nullable=False) is_circulating = Column(Boolean, default=True) + added_at = Column(DateTime, server_default=func.now()) + + book = relationship("Book", back_populates="instances") diff --git a/src/lending/domain.toml b/src/lending/domain.toml index feff6f7..4501cec 100644 --- a/src/lending/domain.toml +++ b/src/lending/domain.toml @@ -9,15 +9,28 @@ command_processing = "sync" [databases.default] provider = "memory" +[databases.production] +provider = "postgresql" +database_uri = "postgresql://postgres:postgres@localhost:5432/postgres" + [databases.memory] provider = "memory" [brokers.default] provider = "inline" +[brokers.production] +provider = "redis" +URI = "redis://127.0.0.1:6379/0" +IS_ASYNC = true + [caches.default] provider = "memory" +[event_store] +provider = "message_db" +database_uri = "postgresql://message_store@localhost:5433/message_store" + [custom] CHECKOUT_PERIOD = 60 # Days HOLD_EXPIRY_DAYS = 7 # Days \ No newline at end of file diff --git a/src/lending/model/book.py b/src/lending/model/book.py index c2df5b1..094ad24 100644 --- a/src/lending/model/book.py +++ b/src/lending/model/book.py @@ -1,6 +1,6 @@ from enum import Enum -from protean import handle +from protean import UnitOfWork, current_domain, handle from protean.fields import String from lending.domain import lending @@ -34,6 +34,12 @@ class Book: ) +@lending.repository(part_of=Book) +class BookRepository: + def find_by_isbn(self, isbn): + return current_domain.repository_for(Book)._dao.find_by(isbn=isbn) + + @lending.event_handler(part_of=Book, stream_category="library::patron") class PatronHoldEventsHandler: @handle(HoldPlaced) @@ -44,3 +50,22 @@ def mark_book_on_hold(self, event: HoldPlaced): book.status = BookStatus.ON_HOLD.value repo.add(book) + + +@lending.subscriber(channel="book_instance_added") +class AddBookToLibrary: + def __call__(self, message: dict): + with UnitOfWork(): + repo = lending.repository_for(Book) + + book_type = ( + BookType.CIRCULATING.value + if message["is_circulating"] + else BookType.RESTRICTED.value + ) + book = Book( + isbn=message["isbn"], + book_type=book_type, + status=BookStatus.AVAILABLE.value, + ) + repo.add(book) diff --git a/src/shared/__init__.py b/src/shared/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/shared/events/__init__.py b/src/shared/events/__init__.py new file mode 100644 index 0000000..06e2761 --- /dev/null +++ b/src/shared/events/__init__.py @@ -0,0 +1,3 @@ +from .book_instance_added import BookInstanceAdded + +__all__ = ["BookInstanceAdded"] diff --git a/src/shared/events/book_instance_added.py b/src/shared/events/book_instance_added.py new file mode 100644 index 0000000..a09ff0e --- /dev/null +++ b/src/shared/events/book_instance_added.py @@ -0,0 +1,12 @@ +from protean import BaseEvent +from protean.fields import Boolean, DateTime, Float, Identifier, String, Text + + +class BookInstanceAdded(BaseEvent): + instance_id = Identifier(required=True) + isbn = String(required=True) + title = String(required=True) + summary = Text(required=True) + price = Float(required=True) + is_circulating = Boolean(required=True) + added_at = DateTime(required=True) diff --git a/tests/catalogue/bdd/test_get_db.py b/tests/catalogue/bdd/test_get_db.py new file mode 100644 index 0000000..fbec92c --- /dev/null +++ b/tests/catalogue/bdd/test_get_db.py @@ -0,0 +1,10 @@ +from catalogue.main import get_db +from sqlalchemy.orm import Session + + +def test_get_db(): + db = next(get_db()) + assert db is not None + assert isinstance(db, Session) + assert db.bind is not None + assert db.bind.url.database == "./books.db" diff --git a/tests/lending/bdd/additions/__init__.py b/tests/lending/bdd/additions/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/lending/bdd/additions/features/book_instanced_added.feature b/tests/lending/bdd/additions/features/book_instanced_added.feature new file mode 100644 index 0000000..89cc21a --- /dev/null +++ b/tests/lending/bdd/additions/features/book_instanced_added.feature @@ -0,0 +1,9 @@ +Feature: Check Out a Book + + Scenario: Book instance is added to catalogue + Given the librarian added a CIRCULATING book instance + Then a CIRCULATING book instance is successfully added as available + + Scenario: Restricted Book instance is added to catalogue + Given the librarian added a RESTRICTED book instance + Then a RESTRICTED book instance is successfully added as available \ No newline at end of file diff --git a/tests/lending/bdd/additions/step_defs/addition_steps.py b/tests/lending/bdd/additions/step_defs/addition_steps.py new file mode 100644 index 0000000..e22bbd6 --- /dev/null +++ b/tests/lending/bdd/additions/step_defs/addition_steps.py @@ -0,0 +1,51 @@ +import asyncio + +import pytest +from protean import Engine, current_domain +from pytest_bdd import given, then +from pytest_bdd.parsers import cfparse + +from lending import Book, BookStatus + + +@pytest.fixture(autouse=True) +def auto_set_and_close_loop(): + # Create and set a new loop + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + + yield + + # Close the loop after the test + if not loop.is_closed(): + loop.close() + asyncio.set_event_loop(None) # Explicitly unset the loop + + +@given(cfparse("the librarian added a {book_type} book instance")) +def given_the_librarian_added_a_circulating_book_instance(book_type): + broker = current_domain.brokers["default"] + broker.publish( + "book_instance_added", + { + "instance_id": 1, + "isbn": "9783161484100", + "title": "The Book", + "summary": "A book about books", + "price": 10.0, + "is_circulating": True if book_type == "CIRCULATING" else False, + "added_at": "2021-01-01T00:00:00Z", + }, + ) + + engine = Engine(current_domain, test_mode=True) + engine.run() + + +@then(cfparse("a {book_type} book instance is successfully added as available")) +def then_the_book_instance_is_successfully_added_as_available(book_type): + book = current_domain.repository_for(Book).find_by_isbn("9783161484100") + + assert book is not None + assert book.book_type == book_type + assert book.status == BookStatus.AVAILABLE.value diff --git a/tests/lending/bdd/additions/test_additions.py b/tests/lending/bdd/additions/test_additions.py new file mode 100644 index 0000000..3dad7d4 --- /dev/null +++ b/tests/lending/bdd/additions/test_additions.py @@ -0,0 +1,5 @@ +from pytest_bdd import scenarios + +from .step_defs.addition_steps import * # noqa: F403 + +scenarios("./features") diff --git a/tests/lending/bdd/checkouts/step_defs/checkout_steps.py b/tests/lending/bdd/checkouts/step_defs/checkout_steps.py index 4c16415..090ca89 100644 --- a/tests/lending/bdd/checkouts/step_defs/checkout_steps.py +++ b/tests/lending/bdd/checkouts/step_defs/checkout_steps.py @@ -1,8 +1,8 @@ from datetime import date, timedelta import pytest +from protean import current_domain, g from protean.exceptions import ValidationError -from protean.globals import current_domain, g from pytest_bdd import given, then, when from pytest_bdd.parsers import cfparse diff --git a/tests/lending/bdd/daily_sheet/step_defs/daily_sheet_steps.py b/tests/lending/bdd/daily_sheet/step_defs/daily_sheet_steps.py index c6b82a7..4ce7ea8 100644 --- a/tests/lending/bdd/daily_sheet/step_defs/daily_sheet_steps.py +++ b/tests/lending/bdd/daily_sheet/step_defs/daily_sheet_steps.py @@ -1,9 +1,8 @@ from datetime import date, timedelta import pytest -from protean import UnitOfWork +from protean import UnitOfWork, current_domain, g from protean.exceptions import ValidationError -from protean.globals import current_domain, g from pytest_bdd import given, then, when from pytest_bdd.parsers import cfparse @@ -207,22 +206,27 @@ def patron_return_book(): def generate_daily_sheet(patron, book): # Log in Patron g.current_user = patron + current_domain.repository_for(Patron).add(g.current_user) # Persist Book g.current_book = book current_domain.repository_for(Book).add(book) # Place Hold - place_hold(g.current_user, g.current_book, "1", HoldType.CLOSED_ENDED)() - current_domain.publish(g.current_user._events) + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, g.current_book, "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) # Expire Hold - g.current_user.holds[0].expires_on = date.today() - timedelta(days=1) + refreshed_patron = current_domain.repository_for(Patron).get(patron.id) + refreshed_patron.holds[0].expires_on = date.today() - timedelta(days=1) + current_domain.repository_for(Patron).add(refreshed_patron) # Update DailySheet's expiry + refreshed_patron = current_domain.repository_for(Patron).get(patron.id) daily_sheet_repo = current_domain.repository_for(DailySheet) record = daily_sheet_repo.find_hold_for_patron( - g.current_user.id, g.current_user.holds[0].id + refreshed_patron.id, refreshed_patron.holds[0].id ) record.hold_expires_on = date.today() - timedelta(days=1) daily_sheet_repo.add(record) @@ -232,22 +236,27 @@ def generate_daily_sheet(patron, book): def generate_daily_sheet_for_overdue_checkouts(patron, book): # Log in Patron g.current_user = patron + current_domain.repository_for(Patron).add(g.current_user) # Persist Book g.current_book = book current_domain.repository_for(Book).add(book) # Checkout Book - checkout(g.current_user, g.current_book, "1")() - current_domain.publish(g.current_user._events) + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + checkout(refreshed_patron, g.current_book, "1")() + current_domain.repository_for(Patron).add(refreshed_patron) # Update Book's Due Date - g.current_user.checkouts[0].due_on = date.today() - timedelta(days=1) + refreshed_patron = current_domain.repository_for(Patron).get(patron.id) + refreshed_patron.checkouts[0].due_on = date.today() - timedelta(days=1) + current_domain.repository_for(Patron).add(refreshed_patron) # Update DailySheet's Due Date + refreshed_patron = current_domain.repository_for(Patron).get(patron.id) daily_sheet_repo = current_domain.repository_for(DailySheet) record = daily_sheet_repo.find_checkout_for_patron( - g.current_user.id, g.current_user.checkouts[0].id + refreshed_patron.id, refreshed_patron.checkouts[0].id ) record.checkout_due_on = date.today() - timedelta(days=1) daily_sheet_repo.add(record) diff --git a/tests/lending/bdd/holds/step_defs/hold_steps.py b/tests/lending/bdd/holds/step_defs/hold_steps.py index 4e4e921..02a7b33 100644 --- a/tests/lending/bdd/holds/step_defs/hold_steps.py +++ b/tests/lending/bdd/holds/step_defs/hold_steps.py @@ -1,17 +1,17 @@ from datetime import date, timedelta import pytest +from protean import current_domain, g from protean.exceptions import ValidationError -from protean.globals import current_domain, g from pytest_bdd import given, then, when from lending import ( Book, BookStatus, - BookType, DailySheetService, HoldStatus, HoldType, + Patron, place_hold, ) @@ -29,16 +29,13 @@ def reset_globals(): @given("a circulating book is available") -def circulating_book(book): - current_domain.repository_for(Book).add(book) - g.current_book = book +def circulating_book(circulating_book): + g.current_book = circulating_book @given("a restricted book is available") -def restricted_book(book): - book.book_type = BookType.RESTRICTED.value - current_domain.repository_for(Book).add(book) - g.current_book = book +def restricted_book(restricted_book): + g.current_book = restricted_book @given("a regular patron is logged in") @@ -56,34 +53,44 @@ def a_researcher_patron(researcher_patron): @given("a book is already on hold by another patron") def already_held_book(book): book.status = BookStatus.ON_HOLD.value + current_domain.repository_for(Book).add(book) g.current_book = book @given("a patron has more than two overdue checkouts at the branch") -def more_than_two_overdue_checkouts(overdue_checkouts_patron, book): +def more_than_two_overdue_checkouts(overdue_checkouts_patron): g.current_user = overdue_checkouts_patron @given("a closed-ended hold is placed") def closed_ended_hold_placed(): - place_hold(g.current_user, g.current_book, "1", HoldType.CLOSED_ENDED)() + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, g.current_book, "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) @given("the hold has reached its expiry date") def hold_expired(): - g.current_user.holds[0].expires_on = date.today() - timedelta(days=1) + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + refreshed_patron.holds[0].expires_on = date.today() - timedelta(days=1) + current_domain.repository_for(Patron).add(refreshed_patron) @given("patron has fewer than five holds") def patron_with_fewer_than_five_holds(): - assert len(g.current_user.holds) < 5 + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + assert len(refreshed_patron.holds) < 5 @given("patron has exactly five holds") def patron_with_exactly_five_holds(five_books): - for i in range(5 - len(g.current_user.holds)): - place_hold(g.current_user, five_books[i], "1", HoldType.CLOSED_ENDED)() - assert len(g.current_user.holds) == 5 + for i in range(5): + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, five_books[i], "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) + + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + assert len(refreshed_patron.holds) == 5 @given("a patron has an active hold") @@ -91,7 +98,9 @@ def patron_with_active_hold(patron, book): g.current_user = patron g.current_book = book - place_hold(g.current_user, book, "1", HoldType.CLOSED_ENDED)() + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, book, "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) @given("a patron has an expired hold") @@ -99,8 +108,13 @@ def patron_with_expired_hold(patron, book): g.current_user = patron g.current_book = book - place_hold(g.current_user, book, "1", HoldType.CLOSED_ENDED)() - g.current_user.holds[0].expires_on = date.today() - timedelta(days=1) + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, book, "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) + + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + refreshed_patron.holds[0].expires_on = date.today() - timedelta(days=1) + current_domain.repository_for(Patron).add(refreshed_patron) @given("a patron has a hold that has been checked out") @@ -108,8 +122,13 @@ def patron_with_checked_out_hold(patron, book): g.current_user = patron g.current_book = book - place_hold(g.current_user, book, "1", HoldType.CLOSED_ENDED)() - g.current_user.holds[0].status = HoldStatus.CHECKED_OUT.value + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, book, "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) + + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + refreshed_patron.holds[0].status = HoldStatus.CHECKED_OUT.value + current_domain.repository_for(Patron).add(refreshed_patron) @when("the patron places a hold on the book") @@ -118,8 +137,9 @@ def patron_with_checked_out_hold(patron, book): @when("the patron tries to place an additional hold") def place_hold_on_book(): try: - place_hold(g.current_user, g.current_book, "1", HoldType.CLOSED_ENDED)() - current_domain.publish(g.current_user._events) + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, g.current_book, "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) except ValidationError as exc: g.current_exception = exc @@ -128,7 +148,9 @@ def place_hold_on_book(): @when("the patron tries to place an open-ended hold") def place_open_ended_hold(): try: - place_hold(g.current_user, g.current_book, "1", HoldType.OPEN_ENDED)() + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, g.current_book, "1", HoldType.OPEN_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) except ValidationError as exc: g.current_exception = exc @@ -136,41 +158,50 @@ def place_open_ended_hold(): @when("the patron places a closed-ended hold") def closed_ended_hold(): try: - place_hold(g.current_user, g.current_book, "1", HoldType.CLOSED_ENDED)() + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, g.current_book, "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) except ValidationError as exc: g.current_exception = exc @when("the system checks for expiring holds") def check_expiring_holds(): - DailySheetService(patrons=[g.current_user]).run() + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + DailySheetService(patrons=[refreshed_patron]).run() + current_domain.repository_for(Patron).add(refreshed_patron) @when("the patron places more than five holds") def place_more_than_five_holds(five_books): for i in range(5): - place_hold(g.current_user, five_books[i], "1", HoldType.CLOSED_ENDED)() + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, five_books[i], "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) # Place one more hold - place_hold(g.current_user, g.current_book, "1", HoldType.CLOSED_ENDED)() + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + place_hold(refreshed_patron, g.current_book, "1", HoldType.CLOSED_ENDED)() + current_domain.repository_for(Patron).add(refreshed_patron) @when("the patron cancels the hold") @when("the patron tries to cancel the hold") def cancel_hold(): - patron = g.current_user + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) try: - patron.cancel_hold(patron.holds[0].id) + refreshed_patron.cancel_hold(refreshed_patron.holds[0].id) + current_domain.repository_for(Patron).add(refreshed_patron) except ValidationError as exc: g.current_exception = exc @then("the hold is successfully placed") def hold_placed(): - patron = g.current_user - assert len(patron.holds) == 1 - assert patron.holds[0].book_id == g.current_book.id + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + assert len(refreshed_patron.holds) == 1 + assert refreshed_patron.holds[0].book_id == g.current_book.id if hasattr(g, "current_exception"): print(g.current_exception.messages) @@ -193,10 +224,11 @@ def confirm_book_not_marked_as_held(): @then("all holds are successfully placed") def holds_placed(five_books): - patron = g.current_user - assert len(patron.holds) == 6 - assert patron.holds[0].book_id == five_books[0].id - assert patron.holds[5].book_id == g.current_book.id + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + + assert len(refreshed_patron.holds) == 6 + assert refreshed_patron.holds[0].book_id == five_books[0].id + assert refreshed_patron.holds[5].book_id == g.current_book.id if hasattr(g, "current_exception"): print(g.current_exception.messages) @@ -211,20 +243,14 @@ def hold_rejected(): @then("the hold status is updated to expired") def check_hold_expired(): - assert g.current_user.holds[0].status == HoldStatus.EXPIRED.value - - # Confirm HoldExpired not in events - assert "HoldExpired" in [ - event.__class__.__name__ for event in g.current_user._events - ] + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + assert refreshed_patron.holds[0].status == HoldStatus.EXPIRED.value @then("the hold is successfully canceled") def check_hold_canceled(): - assert g.current_user.holds[0].status == HoldStatus.CANCELLED.value - - # HoldCancelled is raised after HoldPlaced - assert g.current_user._events[1].__class__.__name__ == "HoldCancelled" + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + assert refreshed_patron.holds[0].status == HoldStatus.CANCELLED.value @then("the cancellation is rejected") @@ -232,12 +258,8 @@ def check_hold_cancellation_rejected(): assert hasattr(g, "current_exception") assert isinstance(g.current_exception, ValidationError) - # Confirm HoldCancelled not in events - assert "HoldCancelled" not in [ - event.__class__.__name__ for event in g.current_user._events - ] - @then("the hold does not have an expiry date") def confirm_no_expiry_date(): - assert g.current_user.holds[0].expires_on is None + refreshed_patron = current_domain.repository_for(Patron).get(g.current_user.id) + assert refreshed_patron.holds[0].expires_on is None diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index 6d34d6c..9239110 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -3,6 +3,7 @@ import pytest from faker import Faker +from protean import current_domain import lending @@ -37,7 +38,7 @@ def run_around_tests(): """Fixture to automatically cleanup infrastructure after every test""" yield - from protean.globals import current_domain + from protean import current_domain # Clear all databases for _, provider in current_domain.providers.items(): @@ -54,18 +55,22 @@ def run_around_tests(): @pytest.fixture def patron(): - return lending.Patron() + new_patron = lending.Patron() + current_domain.repository_for(lending.Patron).add(new_patron) + return new_patron @pytest.fixture def regular_patron(patron): patron.patron_type = lending.PatronType.REGULAR.value + current_domain.repository_for(lending.Patron).add(patron) return patron @pytest.fixture def researcher_patron(patron): patron.patron_type = lending.PatronType.RESEARCHER.value + current_domain.repository_for(lending.Patron).add(patron) return patron @@ -94,28 +99,39 @@ def overdue_checkouts_patron(patron): due_on=checkout_date3.date() + timedelta(days=60), ), ] + current_domain.repository_for(lending.Patron).add(patron) return patron @pytest.fixture def book(): - return lending.Book( + book = lending.Book( isbn=fake.isbn13(), ) + current_domain.repository_for(lending.Book).add(book) + return book + @pytest.fixture def five_books(): - return [lending.Book(isbn=fake.isbn13()) for _ in range(5)] + five_books = [lending.Book(isbn=fake.isbn13()) for _ in range(5)] + + for book in five_books: + current_domain.repository_for(lending.Book).add(book) + + return five_books @pytest.fixture def circulating_book(book): book.book_type = lending.BookType.CIRCULATING.value + current_domain.repository_for(lending.Book).add(book) return book @pytest.fixture def restricted_book(book): book.book_type = lending.BookType.RESTRICTED.value + current_domain.repository_for(lending.Book).add(book) return book diff --git a/tests/lending/tdd/patron/test_checkout_entity.py b/tests/lending/tdd/patron/test_checkout_entity.py index 84c1a5c..7b5a913 100644 --- a/tests/lending/tdd/patron/test_checkout_entity.py +++ b/tests/lending/tdd/patron/test_checkout_entity.py @@ -1,5 +1,5 @@ -from protean.reflection import declared_fields from protean.utils import DomainObjects +from protean.utils.reflection import declared_fields from lending import Checkout diff --git a/tests/lending/tdd/patron/test_hold_entity.py b/tests/lending/tdd/patron/test_hold_entity.py index df5bd8e..877f40c 100644 --- a/tests/lending/tdd/patron/test_hold_entity.py +++ b/tests/lending/tdd/patron/test_hold_entity.py @@ -1,5 +1,5 @@ -from protean.reflection import declared_fields from protean.utils import DomainObjects +from protean.utils.reflection import declared_fields from lending import Hold diff --git a/tests/lending/tdd/patron/test_patron_aggregate.py b/tests/lending/tdd/patron/test_patron_aggregate.py index 0c426e5..b6fa0cb 100644 --- a/tests/lending/tdd/patron/test_patron_aggregate.py +++ b/tests/lending/tdd/patron/test_patron_aggregate.py @@ -7,8 +7,8 @@ - Checkouts - HasMany """ -from protean.reflection import declared_fields from protean.utils import DomainObjects +from protean.utils.reflection import declared_fields from lending import Patron