diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..f561429 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,10 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.4.9 + hooks: + # Run the linter. + - id: ruff + args: [ "check", "--select", "I", "--fix" ] + # Run the formatter. + - id: ruff-format diff --git a/poetry.lock b/poetry.lock index 3d05b94..fd5071d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -91,6 +91,17 @@ files = [ {file = "certifi-2024.6.2.tar.gz", hash = "sha256:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516"}, ] +[[package]] +name = "cfgv" +version = "3.4.0" +description = "Validate configuration and produce human readable error messages." +optional = false +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] + [[package]] name = "chardet" version = "5.2.0" @@ -347,6 +358,17 @@ files = [ {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, ] +[[package]] +name = "distlib" +version = "0.3.8" +description = "Distribution utilities" +optional = false +python-versions = "*" +files = [ + {file = "distlib-0.3.8-py2.py3-none-any.whl", hash = "sha256:034db59a0b96f8ca18035f36290806a9a6e6bd9d1ff91e45a7f172eb17e51784"}, + {file = "distlib-0.3.8.tar.gz", hash = "sha256:1530ea13e350031b6312d8580ddb6b27a104275a31106523b8f123787f494f64"}, +] + [[package]] name = "dunamai" version = "1.21.1" @@ -389,6 +411,22 @@ files = [ [package.dependencies] python-dateutil = ">=2.4" +[[package]] +name = "filelock" +version = "3.15.1" +description = "A platform independent file lock." +optional = false +python-versions = ">=3.8" +files = [ + {file = "filelock-3.15.1-py3-none-any.whl", hash = "sha256:71b3102950e91dfc1bb4209b64be4dc8854f40e5f534428d8684f953ac847fac"}, + {file = "filelock-3.15.1.tar.gz", hash = "sha256:58a2549afdf9e02e10720eaa4d4470f56386d7a6f72edd7d0596337af8ed7ad8"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.3.2)", "diff-cover (>=8.0.1)", "pytest (>=7.4.3)", "pytest-asyncio (>=0.21)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)", "pytest-timeout (>=2.2)"] +typing = ["typing-extensions (>=4.8)"] + [[package]] name = "funcy" version = "2.0" @@ -400,6 +438,20 @@ files = [ {file = "funcy-2.0.tar.gz", hash = "sha256:3963315d59d41c6f30c04bc910e10ab50a3ac4a225868bfa96feed133df075cb"}, ] +[[package]] +name = "identify" +version = "2.5.36" +description = "File identification library for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "identify-2.5.36-py2.py3-none-any.whl", hash = "sha256:37d93f380f4de590500d9dba7db359d0d3da95ffe7f9de1753faa159e71e7dfa"}, + {file = "identify-2.5.36.tar.gz", hash = "sha256:e5e00f54165f9047fbebeb4a560f9acfb8af4c88232be60a488e9b68d122745d"}, +] + +[package.extras] +license = ["ukkonen"] + [[package]] name = "idna" version = "3.7" @@ -680,6 +732,17 @@ files = [ {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, ] +[[package]] +name = "nodeenv" +version = "1.9.1" +description = "Node.js virtual environment builder" +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + [[package]] name = "packaging" version = "24.0" @@ -762,6 +825,22 @@ files = [ [package.dependencies] ptyprocess = ">=0.5" +[[package]] +name = "platformdirs" +version = "4.2.2" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." +optional = false +python-versions = ">=3.8" +files = [ + {file = "platformdirs-4.2.2-py3-none-any.whl", hash = "sha256:2d7a1657e36a80ea911db832a8a6ece5ee53d8de21edd5cc5879af6530b1bfee"}, + {file = "platformdirs-4.2.2.tar.gz", hash = "sha256:38b7b51f512eed9e84a22788b4bce1de17c0adb134d6becb09836e37d8654cd3"}, +] + +[package.extras] +docs = ["furo (>=2023.9.10)", "proselint (>=0.13)", "sphinx (>=7.2.6)", "sphinx-autodoc-typehints (>=1.25.2)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=7.4.3)", "pytest-cov (>=4.1)", "pytest-mock (>=3.12)"] +type = ["mypy (>=1.8)"] + [[package]] name = "pluggy" version = "1.5.0" @@ -796,6 +875,24 @@ dev = ["paramiko", "psutil", "pytest (>=6.0)", "pytest-cov", "pytest-mock", "pyt docs = ["sphinx (>=4.0.0)", "sphinx-rtd-theme (>=1.0.0)"] ssh = ["paramiko"] +[[package]] +name = "pre-commit" +version = "3.7.1" +description = "A framework for managing and maintaining multi-language pre-commit hooks." +optional = false +python-versions = ">=3.9" +files = [ + {file = "pre_commit-3.7.1-py2.py3-none-any.whl", hash = "sha256:fae36fd1d7ad7d6a5a1c0b0d5adb2ed1a3bda5a21bf6c3e5372073d7a11cd4c5"}, + {file = "pre_commit-3.7.1.tar.gz", hash = "sha256:8ca3ad567bc78a4972a3f1a477e94a79d4597e8140a6e0b651c5e33899c3654a"}, +] + +[package.dependencies] +cfgv = ">=2.0.0" +identify = ">=1.0.0" +nodeenv = ">=0.11.1" +pyyaml = ">=5.1" +virtualenv = ">=20.10.0" + [[package]] name = "prompt-toolkit" version = "3.0.45" @@ -1373,6 +1470,26 @@ h2 = ["h2 (>=4,<5)"] socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] zstd = ["zstandard (>=0.18.0)"] +[[package]] +name = "virtualenv" +version = "20.26.2" +description = "Virtual Python Environment builder" +optional = false +python-versions = ">=3.7" +files = [ + {file = "virtualenv-20.26.2-py3-none-any.whl", hash = "sha256:a624db5e94f01ad993d476b9ee5346fdf7b9de43ccaee0e0197012dc838a0e9b"}, + {file = "virtualenv-20.26.2.tar.gz", hash = "sha256:82bf0f4eebbb78d36ddaee0283d43fe5736b53880b8a8cdcd37390a07ac3741c"}, +] + +[package.dependencies] +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" + +[package.extras] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] + [[package]] name = "wcwidth" version = "0.2.13" @@ -1415,4 +1532,4 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.0" python-versions = "^3.11" -content-hash = "1b0cab55ab7d11edf76451cb3f5d64c03978a3b442ace9f33a05ce76f27aba3f" +content-hash = "f76ab09f4eabea611936b965e21ac30ccac2e1941ce06fb378b554425e0ac087" diff --git a/pyproject.toml b/pyproject.toml index 3f76e46..f4d0c53 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -27,6 +27,7 @@ pytest-cov = "^5.0.0" [tool.poetry.group.dev.dependencies] ruff = "^0.4.7" +pre-commit = "^3.7.1" [build-system] requires = ["poetry-core"] @@ -41,3 +42,7 @@ addopts = [ "--cov-report=term-missing", "--cov-branch" ] + +[tool.ruff.lint.isort] +known-first-party = ["lending"] +known-third-party = ["protean"] \ No newline at end of file diff --git a/src/lending/__init__.py b/src/lending/__init__.py index 5db538d..004d5ef 100644 --- a/src/lending/__init__.py +++ b/src/lending/__init__.py @@ -1,23 +1,21 @@ -from lending.patron import ( - Patron, - PatronType, - Hold, - HoldType, - HoldStatus, - Checkout, - CheckoutStatus, - HoldPlaced -) - from lending.book import ( Book, BookStatus, BookType, ) - -from lending.holding_service import place_hold -from lending.daily_sheet_service import DailySheetService from lending.checkout_service import checkout +from lending.daily_sheet_service import DailySheetService +from lending.holding_service import place_hold +from lending.patron import ( + Checkout, + CheckoutStatus, + Hold, + HoldPlaced, + HoldStatus, + HoldType, + Patron, + PatronType, +) __all__ = [ "Patron", diff --git a/src/lending/book.py b/src/lending/book.py index 0b6cadf..e805248 100644 --- a/src/lending/book.py +++ b/src/lending/book.py @@ -3,8 +3,8 @@ from protean import handle from protean.fields import String -from lending.domain import lending from lending import HoldPlaced +from lending.domain import lending class BookStatus(Enum): diff --git a/src/lending/checkout_service.py b/src/lending/checkout_service.py index 71cc56a..f496ffa 100644 --- a/src/lending/checkout_service.py +++ b/src/lending/checkout_service.py @@ -1,11 +1,9 @@ -from datetime import datetime, timedelta - from protean import invariant from protean.exceptions import ValidationError from protean.fields import Identifier +from lending import Book, BookType, Checkout, HoldStatus, Patron, PatronType from lending.domain import lending -from lending import Patron, Book, Checkout, HoldStatus, BookType, PatronType @lending.domain_service(part_of=[Patron, Book]) @@ -34,8 +32,6 @@ def __call__(self): Checkout( book_id=self.book.id, branch_id=self.branch_id, - checkout_date=datetime.now(), - due_date=datetime.now() + timedelta(days=60), ) ) diff --git a/src/lending/daily_sheet_service.py b/src/lending/daily_sheet_service.py index baba3cd..1f763b5 100644 --- a/src/lending/daily_sheet_service.py +++ b/src/lending/daily_sheet_service.py @@ -1,6 +1,6 @@ -from datetime import datetime, timezone +from datetime import date -from lending import Patron, Book, HoldStatus +from lending import Book, HoldStatus, Patron from lending.domain import lending @@ -18,12 +18,12 @@ def _expire_holds(self): for hold in patron.holds: if ( hold.status == HoldStatus.ACTIVE.value - and hold.expiry_date < datetime.now() + and hold.expiry_date < date.today() ): patron.expire_hold(hold.id) def _overdue_checkouts(self): for patron in self.patrons: for checkout in patron.checkouts: - if checkout.due_date < datetime.now(timezone.utc): + if checkout.due_date < date.today(): checkout.overdue() diff --git a/src/lending/domain.toml b/src/lending/domain.toml index a860198..dfd09df 100644 --- a/src/lending/domain.toml +++ b/src/lending/domain.toml @@ -16,4 +16,7 @@ provider = "memory" provider = "inline" [caches.default] -provider = "memory" \ No newline at end of file +provider = "memory" + +[custom] +CHECKOUT_PERIOD = 60 # Days \ No newline at end of file diff --git a/src/lending/holding_service.py b/src/lending/holding_service.py index bcad986..dcd370d 100644 --- a/src/lending/holding_service.py +++ b/src/lending/holding_service.py @@ -1,19 +1,19 @@ -from datetime import datetime, timedelta +from datetime import date, datetime, timedelta from protean import invariant -from protean.fields import Identifier from protean.exceptions import ValidationError +from protean.fields import Identifier from lending import ( + Book, + BookStatus, + BookType, Hold, + HoldPlaced, HoldStatus, HoldType, - HoldPlaced, Patron, PatronType, - Book, - BookType, - BookStatus, ) from lending.domain import lending @@ -72,8 +72,7 @@ def patron_cannot_not_have_more_than_two_overdue_checkouts_in_branch(self): overdue_checkouts_in_branch = [ checkout for checkout in self.patron.checkouts - if checkout.due_date < datetime.now() - and checkout.branch_id == self.branch_id + if checkout.due_date < date.today() and checkout.branch_id == self.branch_id ] if len(overdue_checkouts_in_branch) > 2: raise ValidationError( @@ -91,7 +90,7 @@ def __call__(self): hold_type=self.hold_type.value, status=HoldStatus.ACTIVE.value, request_date=datetime.now(), - expiry_date=datetime.now() + timedelta(days=7), + expiry_date=date.today() + timedelta(days=7), ) self.patron.add_holds(hold) @@ -104,6 +103,6 @@ def __call__(self): branch_id=hold.branch_id, hold_type=hold.hold_type, request_date=hold.request_date, - expiry_date=hold.expiry_date + expiry_date=hold.expiry_date, ) ) diff --git a/src/lending/patron.py b/src/lending/patron.py index 716245a..53ab2ca 100644 --- a/src/lending/patron.py +++ b/src/lending/patron.py @@ -1,8 +1,8 @@ -from datetime import datetime, timezone, timedelta +from datetime import date, datetime, timedelta, timezone from enum import Enum from protean.exceptions import ObjectNotFoundError, ValidationError -from protean.fields import DateTime, HasMany, Identifier, String +from protean.fields import Date, DateTime, HasMany, Identifier, String from lending.domain import lending @@ -65,13 +65,13 @@ class Hold: hold_type = String(max_length=12, default=HoldType.CLOSED_ENDED.value) status = String(max_length=11, default=HoldStatus.ACTIVE.value) request_date = DateTime(required=True) - expiry_date = DateTime(required=True) + expiry_date = Date(required=True) def expire(self): self.status = HoldStatus.EXPIRED.value def cancel(self): - if self.status == HoldStatus.EXPIRED.value or self.expiry_date < datetime.now(): + if self.status == HoldStatus.EXPIRED.value or self.expiry_date < date.today(): raise ValidationError({"expired_hold": ["Cannot cancel expired holds"]}) if self.status == HoldStatus.CHECKED_OUT.value: @@ -95,7 +95,11 @@ class Checkout: branch_id = Identifier(required=True) checkout_date = DateTime(required=True, default=utc_now) status = String(max_length=11, default=CheckoutStatus.ACTIVE.value) - due_date = DateTime(required=True, default=lambda: utc_now() + timedelta(days=7)) + due_date = Date( + required=True, + default=lambda: date.today() + + timedelta(days=lending.config["custom"]["CHECKOUT_PERIOD"]), + ) return_date = DateTime() def return_(self): diff --git a/tests/lending/bdd/checkouts/features/check_out_a_book.feature b/tests/lending/bdd/checkouts/features/check_out_a_book.feature index 86fd0fd..9df6f3e 100644 --- a/tests/lending/bdd/checkouts/features/check_out_a_book.feature +++ b/tests/lending/bdd/checkouts/features/check_out_a_book.feature @@ -6,6 +6,7 @@ Feature: Check Out a Book And the patron has a hold on the book When the patron checks out the book Then the checkout is successfully completed + And the checkout has a validity of CHECKOUT_PERIOD Scenario: Patron checks out an available circulating book Given a circulating book is available diff --git a/tests/lending/bdd/checkouts/step_defs/checkout_steps.py b/tests/lending/bdd/checkouts/step_defs/checkout_steps.py index 3c32a03..1990405 100644 --- a/tests/lending/bdd/checkouts/step_defs/checkout_steps.py +++ b/tests/lending/bdd/checkouts/step_defs/checkout_steps.py @@ -1,21 +1,21 @@ -import pytest - -from datetime import datetime, timedelta, timezone - -from pytest_bdd import given, when, then +from datetime import date, timedelta +import pytest from protean.exceptions import ValidationError -from protean.globals import g - -from lending import Book, Patron, Checkout +from protean.globals import current_domain, g +from pytest_bdd import given, then, when +from pytest_bdd.parsers import cfparse from lending import ( - place_hold, - HoldType, - checkout, + Book, BookType, + Checkout, CheckoutStatus, DailySheetService, + HoldType, + Patron, + checkout, + place_hold, ) @@ -89,7 +89,7 @@ def system_has_overdue_checkouts(): ] ) # Manually expire a checkout - patron1.checkouts[0].due_date = datetime.now(timezone.utc) - timedelta(days=1) + patron1.checkouts[0].due_date = date.today() - timedelta(days=1) patron2.add_checkouts( Checkout( @@ -98,7 +98,7 @@ def system_has_overdue_checkouts(): ) ) # Manually exipre a checkout - patron2.checkouts[0].due_date = datetime.now(timezone.utc) - timedelta(days=1) + patron2.checkouts[0].due_date = date.today() - timedelta(days=1) g.current_patrons = [patron1, patron2] @@ -131,6 +131,14 @@ def confirm_checkout_book(): assert g.current_user.checkouts[0].book_id == g.current_book.id +@then(cfparse("the checkout has a validity of {validity_days_config}")) +def confirm_checkout_expiry(validity_days_config): + checkout = g.current_user.checkouts[0] + assert checkout.due_date == date.today() + timedelta( + days=current_domain.config["custom"][validity_days_config] + ) + + @then("the checkout is rejected") def confirm_checkout_rejected(): assert hasattr(g, "current_exception") diff --git a/tests/lending/bdd/holds/features/place_a_hold_on_a_book.feature b/tests/lending/bdd/holds/features/place_a_hold_on_a_book.feature index 1b033ae..a12ab9d 100644 --- a/tests/lending/bdd/holds/features/place_a_hold_on_a_book.feature +++ b/tests/lending/bdd/holds/features/place_a_hold_on_a_book.feature @@ -37,7 +37,7 @@ Feature: Place a Hold on a Book Scenario: Patron with two overdue checkouts at the branch tries to place a hold Given a circulating book is available And a patron has more than two overdue checkouts at the branch - And a patron is logged in + And the patron is logged in When the patron tries to place a hold on a book Then the hold placement is rejected And the book is not marked as held diff --git a/tests/lending/bdd/holds/step_defs/hold_steps.py b/tests/lending/bdd/holds/step_defs/hold_steps.py index b92c05e..06a6a33 100644 --- a/tests/lending/bdd/holds/step_defs/hold_steps.py +++ b/tests/lending/bdd/holds/step_defs/hold_steps.py @@ -1,20 +1,18 @@ -import pytest - -from datetime import datetime, timedelta - -from pytest_bdd import given, when, then +from datetime import date, timedelta +import pytest from protean.exceptions import ValidationError -from protean.globals import g, current_domain +from protean.globals import current_domain, g +from pytest_bdd import given, then, when from lending import ( Book, BookStatus, BookType, - place_hold, - HoldType, - HoldStatus, DailySheetService, + HoldStatus, + HoldType, + place_hold, ) @@ -45,6 +43,7 @@ def restricted_book(book): @given("a regular patron is logged in") @given("a patron is logged in") +@given("the patron is logged in") def regular_patron(regular_patron): g.current_user = regular_patron @@ -72,7 +71,7 @@ def closed_ended_hold_placed(): @given("the hold has reached its expiry date") def hold_expired(): - g.current_user.holds[0].expiry_date = datetime.now() - timedelta(days=1) + g.current_user.holds[0].expiry_date = date.today() - timedelta(days=1) @given("patron has fewer than five holds") @@ -101,7 +100,7 @@ def patron_with_expired_hold(patron, book): g.current_book = book place_hold(g.current_user, book, "1", HoldType.CLOSED_ENDED)() - g.current_user.holds[0].expiry_date = datetime.now() - timedelta(days=1) + g.current_user.holds[0].expiry_date = date.today() - timedelta(days=1) @given("a patron has a hold that has been checked out") diff --git a/tests/lending/conftest.py b/tests/lending/conftest.py index b44bf3f..22075bd 100644 --- a/tests/lending/conftest.py +++ b/tests/lending/conftest.py @@ -1,10 +1,11 @@ -import lending import os -import pytest - from datetime import timedelta + +import pytest from faker import Faker +import lending + Faker.seed(0) fake = Faker() @@ -70,9 +71,9 @@ def researcher_patron(patron): @pytest.fixture def overdue_checkouts_patron(patron): - checkout_date1 = fake.date_time_between(start_date="-90d", end_date="-61d") - checkout_date2 = fake.date_time_between(start_date="-80d", end_date="-71d") - checkout_date3 = fake.date_time_between(start_date="-85d", end_date="-75d") + checkout_date1 = fake.date_between(start_date="-90d", end_date="-61d") + checkout_date2 = fake.date_between(start_date="-80d", end_date="-71d") + checkout_date3 = fake.date_between(start_date="-85d", end_date="-75d") patron.checkouts = [ lending.Checkout( branch_id="1", diff --git a/tests/lending/tdd/patron/test_hold_entity.py b/tests/lending/tdd/patron/test_hold_entity.py index 4d279a8..df5bd8e 100644 --- a/tests/lending/tdd/patron/test_hold_entity.py +++ b/tests/lending/tdd/patron/test_hold_entity.py @@ -1,7 +1,6 @@ from protean.reflection import declared_fields from protean.utils import DomainObjects - from lending import Hold