From 6ef8b1849f1d1c806656ea5ed0e4139cec6e523b Mon Sep 17 00:00:00 2001 From: Richa Date: Sat, 21 Sep 2024 13:16:30 +0100 Subject: [PATCH 01/10] inital docs --- .gitignore | 1 + solution/Makefile | 5 +++++ solution/README.md | 37 +++++++++++++++++++++++++++++++++++++ 3 files changed, 43 insertions(+) create mode 100644 .gitignore create mode 100644 solution/Makefile create mode 100644 solution/README.md diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..266c348 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*.venv \ No newline at end of file diff --git a/solution/Makefile b/solution/Makefile new file mode 100644 index 0000000..5e08975 --- /dev/null +++ b/solution/Makefile @@ -0,0 +1,5 @@ +test: + poetry run pytest + +coverage: + poetry run coverage \ No newline at end of file diff --git a/solution/README.md b/solution/README.md new file mode 100644 index 0000000..eb398b1 --- /dev/null +++ b/solution/README.md @@ -0,0 +1,37 @@ +# DI US Shopping - Checkout System + +## Prerequisites +- Install [Python](https://www.python.org/downloads/) (make sure you install a version that is compatible with this project, this can be found in the [pyproject file](pyproject.toml)) +- Install [Poetry](https://python-poetry.org/docs/) to manage your virtual environment. +- Install [Make](https://www.gnu.org/software/make/) to take advantage of shortcuts for running tests. + +## Installing Requirements +- Create your virtual environment by running the following command in a terminal: +``` +python -m venv .venv +``` +- Activate the virtual environment: +``` +source .venv/bin/activate +``` +- Install the requirements using poetry +``` +poetry install +``` +- Check requirements have installed successfully by running: +``` +poetry show +``` +All installed requirements will be returned in blue. If any are not installed they will appear as red. Try running `poetry install` again if this happens, or installing the missing packages individually. + +## Running the App + +## Running Tests +- To run all tests, run: +``` +make test +``` +- To run all tests and receive a coverage report, run: +``` +make coverage +``` \ No newline at end of file From bc5bfa39279b8ad3df7dff1b77f9d5a14af263e0 Mon Sep 17 00:00:00 2001 From: Richa Date: Sat, 21 Sep 2024 13:20:40 +0100 Subject: [PATCH 02/10] poetry init --- solution/poetry.lock | 7 +++++++ solution/pyproject.toml | 15 +++++++++++++++ 2 files changed, 22 insertions(+) create mode 100644 solution/poetry.lock create mode 100644 solution/pyproject.toml diff --git a/solution/poetry.lock b/solution/poetry.lock new file mode 100644 index 0000000..a9b0222 --- /dev/null +++ b/solution/poetry.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. +package = [] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9" diff --git a/solution/pyproject.toml b/solution/pyproject.toml new file mode 100644 index 0000000..a5ad666 --- /dev/null +++ b/solution/pyproject.toml @@ -0,0 +1,15 @@ +[tool.poetry] +name = "dius-shopping" +version = "0.1.0" +description = "Solution to the shopping coding challenge created by DIUS for DataRock." +authors = ["Richa "] +readme = "README.md" +package-mode = false + +[tool.poetry.dependencies] +python = "^3.10" + + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" From 314bc6e7bcf681178dcd91a5e879b5010d589ddd Mon Sep 17 00:00:00 2001 From: Richa Date: Sun, 22 Sep 2024 18:37:05 +0100 Subject: [PATCH 03/10] logic --- .gitignore | 3 +- solution/catalogue.yaml | 20 ++++ solution/checkout.py | 27 +++++ solution/models.py | 19 ++++ solution/poetry.lock | 211 +++++++++++++++++++++++++++++++++++++- solution/pricing_rules.py | 126 +++++++++++++++++++++++ solution/pyproject.toml | 2 + solution/specials.yaml | 16 +++ solution/test.py | 55 ++++++++++ 9 files changed, 476 insertions(+), 3 deletions(-) create mode 100644 solution/catalogue.yaml create mode 100644 solution/checkout.py create mode 100644 solution/models.py create mode 100644 solution/pricing_rules.py create mode 100644 solution/specials.yaml create mode 100644 solution/test.py diff --git a/.gitignore b/.gitignore index 266c348..5110d25 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ -*.venv \ No newline at end of file +*.venv +*__pycache__* \ No newline at end of file diff --git a/solution/catalogue.yaml b/solution/catalogue.yaml new file mode 100644 index 0000000..bcebf0b --- /dev/null +++ b/solution/catalogue.yaml @@ -0,0 +1,20 @@ +ipd: + sku: ipd + name: Super iPad + price: 549.99 + currency: dollar +mbp: + sku: mbp + name: MacBook Pro + price: 1399.99 + currency: dollar +atv: + sku: atv + name: Apple TV + price: 109.50 + currency: dollar +vga: + sku: vga + name: VGA adapter + price: 30.00 + currency: dollar \ No newline at end of file diff --git a/solution/checkout.py b/solution/checkout.py new file mode 100644 index 0000000..ff5d8de --- /dev/null +++ b/solution/checkout.py @@ -0,0 +1,27 @@ +from typing import List, Dict +from models import PricingRule, Item +import logging + +logger = logging.getLogger(__name__) + +class Checkout: + + def __init__(self, pricing_rules: List[PricingRule], catalogue: Dict[str, Item]): + self.pricing_rules = pricing_rules + self.scanned_items = list() + self.catalogue = catalogue + + def scan(self, item_sku: str) -> None: + item_attrs = self.catalogue.get(item_sku) + item = Item(**item_attrs) + self.scanned_items.append(item) + + def total(self): + self.items_with_deals = self.scanned_items.copy() + for rule in self.pricing_rules: + self.items_with_deals = rule.apply(self.items_with_deals) + breakpoint() + return sum(item.price for item in self.items_with_deals) + + def receipt(self): + return self.items_with_deals diff --git a/solution/models.py b/solution/models.py new file mode 100644 index 0000000..bfeafa6 --- /dev/null +++ b/solution/models.py @@ -0,0 +1,19 @@ +from typing import List, Protocol +from pydantic import BaseModel + +class Item(BaseModel): + sku: str + name: str + price: float + currency: str + +class PricingRule(Protocol): + + def __init__(self, *args, **kwargs) -> None: + ... + + def qualifies(self, items: List[Item]) -> bool: + ... + + def apply(self, items: List[Item]) -> List[Item]: + ... \ No newline at end of file diff --git a/solution/poetry.lock b/solution/poetry.lock index a9b0222..da703dd 100644 --- a/solution/poetry.lock +++ b/solution/poetry.lock @@ -1,7 +1,214 @@ # This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand. -package = [] + +[[package]] +name = "annotated-types" +version = "0.7.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"}, + {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, +] + +[[package]] +name = "pydantic" +version = "2.9.2" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.9.2-py3-none-any.whl", hash = "sha256:f048cec7b26778210e28a0459867920654d48e5e62db0958433636cde4254f12"}, + {file = "pydantic-2.9.2.tar.gz", hash = "sha256:d155cef71265d1e9807ed1c32b4c8deec042a44a50a4188b25ac67ecd81a9c0f"}, +] + +[package.dependencies] +annotated-types = ">=0.6.0" +pydantic-core = "2.23.4" +typing-extensions = [ + {version = ">=4.12.2", markers = "python_version >= \"3.13\""}, + {version = ">=4.6.1", markers = "python_version < \"3.13\""}, +] + +[package.extras] +email = ["email-validator (>=2.0.0)"] +timezone = ["tzdata"] + +[[package]] +name = "pydantic-core" +version = "2.23.4" +description = "Core functionality for Pydantic validation and serialization" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:b10bd51f823d891193d4717448fab065733958bdb6a6b351967bd349d48d5c9b"}, + {file = "pydantic_core-2.23.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4fc714bdbfb534f94034efaa6eadd74e5b93c8fa6315565a222f7b6f42ca1166"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63e46b3169866bd62849936de036f901a9356e36376079b05efa83caeaa02ceb"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed1a53de42fbe34853ba90513cea21673481cd81ed1be739f7f2efb931b24916"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:cfdd16ab5e59fc31b5e906d1a3f666571abc367598e3e02c83403acabc092e07"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:255a8ef062cbf6674450e668482456abac99a5583bbafb73f9ad469540a3a232"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a7cd62e831afe623fbb7aabbb4fe583212115b3ef38a9f6b71869ba644624a2"}, + {file = "pydantic_core-2.23.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f09e2ff1f17c2b51f2bc76d1cc33da96298f0a036a137f5440ab3ec5360b624f"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:e38e63e6f3d1cec5a27e0afe90a085af8b6806ee208b33030e65b6516353f1a3"}, + {file = "pydantic_core-2.23.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:0dbd8dbed2085ed23b5c04afa29d8fd2771674223135dc9bc937f3c09284d071"}, + {file = "pydantic_core-2.23.4-cp310-none-win32.whl", hash = "sha256:6531b7ca5f951d663c339002e91aaebda765ec7d61b7d1e3991051906ddde119"}, + {file = "pydantic_core-2.23.4-cp310-none-win_amd64.whl", hash = "sha256:7c9129eb40958b3d4500fa2467e6a83356b3b61bfff1b414c7361d9220f9ae8f"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:77733e3892bb0a7fa797826361ce8a9184d25c8dffaec60b7ffe928153680ba8"}, + {file = "pydantic_core-2.23.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1b84d168f6c48fabd1f2027a3d1bdfe62f92cade1fb273a5d68e621da0e44e6d"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df49e7a0861a8c36d089c1ed57d308623d60416dab2647a4a17fe050ba85de0e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ff02b6d461a6de369f07ec15e465a88895f3223eb75073ffea56b84d9331f607"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:996a38a83508c54c78a5f41456b0103c30508fed9abcad0a59b876d7398f25fd"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d97683ddee4723ae8c95d1eddac7c192e8c552da0c73a925a89fa8649bf13eea"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:216f9b2d7713eb98cb83c80b9c794de1f6b7e3145eef40400c62e86cee5f4e1e"}, + {file = "pydantic_core-2.23.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f783e0ec4803c787bcea93e13e9932edab72068f68ecffdf86a99fd5918878b"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:d0776dea117cf5272382634bd2a5c1b6eb16767c223c6a5317cd3e2a757c61a0"}, + {file = "pydantic_core-2.23.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5f7a395a8cf1621939692dba2a6b6a830efa6b3cee787d82c7de1ad2930de64"}, + {file = "pydantic_core-2.23.4-cp311-none-win32.whl", hash = "sha256:74b9127ffea03643e998e0c5ad9bd3811d3dac8c676e47db17b0ee7c3c3bf35f"}, + {file = "pydantic_core-2.23.4-cp311-none-win_amd64.whl", hash = "sha256:98d134c954828488b153d88ba1f34e14259284f256180ce659e8d83e9c05eaa3"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f3e0da4ebaef65158d4dfd7d3678aad692f7666877df0002b8a522cdf088f231"}, + {file = "pydantic_core-2.23.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f69a8e0b033b747bb3e36a44e7732f0c99f7edd5cea723d45bc0d6e95377ffee"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:723314c1d51722ab28bfcd5240d858512ffd3116449c557a1336cbe3919beb87"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:bb2802e667b7051a1bebbfe93684841cc9351004e2badbd6411bf357ab8d5ac8"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d18ca8148bebe1b0a382a27a8ee60350091a6ddaf475fa05ef50dc35b5df6327"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:33e3d65a85a2a4a0dc3b092b938a4062b1a05f3a9abde65ea93b233bca0e03f2"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:128585782e5bfa515c590ccee4b727fb76925dd04a98864182b22e89a4e6ed36"}, + {file = "pydantic_core-2.23.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:68665f4c17edcceecc112dfed5dbe6f92261fb9d6054b47d01bf6371a6196126"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:20152074317d9bed6b7a95ade3b7d6054845d70584216160860425f4fbd5ee9e"}, + {file = "pydantic_core-2.23.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9261d3ce84fa1d38ed649c3638feefeae23d32ba9182963e465d58d62203bd24"}, + {file = "pydantic_core-2.23.4-cp312-none-win32.whl", hash = "sha256:4ba762ed58e8d68657fc1281e9bb72e1c3e79cc5d464be146e260c541ec12d84"}, + {file = "pydantic_core-2.23.4-cp312-none-win_amd64.whl", hash = "sha256:97df63000f4fea395b2824da80e169731088656d1818a11b95f3b173747b6cd9"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:7530e201d10d7d14abce4fb54cfe5b94a0aefc87da539d0346a484ead376c3cc"}, + {file = "pydantic_core-2.23.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:df933278128ea1cd77772673c73954e53a1c95a4fdf41eef97c2b779271bd0bd"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cb3da3fd1b6a5d0279a01877713dbda118a2a4fc6f0d821a57da2e464793f05"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:42c6dcb030aefb668a2b7009c85b27f90e51e6a3b4d5c9bc4c57631292015b0d"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:696dd8d674d6ce621ab9d45b205df149399e4bb9aa34102c970b721554828510"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2971bb5ffe72cc0f555c13e19b23c85b654dd2a8f7ab493c262071377bfce9f6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8394d940e5d400d04cad4f75c0598665cbb81aecefaca82ca85bd28264af7f9b"}, + {file = "pydantic_core-2.23.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:0dff76e0602ca7d4cdaacc1ac4c005e0ce0dcfe095d5b5259163a80d3a10d327"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:7d32706badfe136888bdea71c0def994644e09fff0bfe47441deaed8e96fdbc6"}, + {file = "pydantic_core-2.23.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ed541d70698978a20eb63d8c5d72f2cc6d7079d9d90f6b50bad07826f1320f5f"}, + {file = "pydantic_core-2.23.4-cp313-none-win32.whl", hash = "sha256:3d5639516376dce1940ea36edf408c554475369f5da2abd45d44621cb616f769"}, + {file = "pydantic_core-2.23.4-cp313-none-win_amd64.whl", hash = "sha256:5a1504ad17ba4210df3a045132a7baeeba5a200e930f57512ee02909fc5c4cb5"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:d4488a93b071c04dc20f5cecc3631fc78b9789dd72483ba15d423b5b3689b555"}, + {file = "pydantic_core-2.23.4-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:81965a16b675b35e1d09dd14df53f190f9129c0202356ed44ab2728b1c905658"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ffa2ebd4c8530079140dd2d7f794a9d9a73cbb8e9d59ffe24c63436efa8f271"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:61817945f2fe7d166e75fbfb28004034b48e44878177fc54d81688e7b85a3665"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:29d2c342c4bc01b88402d60189f3df065fb0dda3654744d5a165a5288a657368"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5e11661ce0fd30a6790e8bcdf263b9ec5988e95e63cf901972107efc49218b13"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d18368b137c6295db49ce7218b1a9ba15c5bc254c96d7c9f9e924a9bc7825ad"}, + {file = "pydantic_core-2.23.4-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ec4e55f79b1c4ffb2eecd8a0cfba9955a2588497d96851f4c8f99aa4a1d39b12"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:374a5e5049eda9e0a44c696c7ade3ff355f06b1fe0bb945ea3cac2bc336478a2"}, + {file = "pydantic_core-2.23.4-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:5c364564d17da23db1106787675fc7af45f2f7b58b4173bfdd105564e132e6fb"}, + {file = "pydantic_core-2.23.4-cp38-none-win32.whl", hash = "sha256:d7a80d21d613eec45e3d41eb22f8f94ddc758a6c4720842dc74c0581f54993d6"}, + {file = "pydantic_core-2.23.4-cp38-none-win_amd64.whl", hash = "sha256:5f5ff8d839f4566a474a969508fe1c5e59c31c80d9e140566f9a37bba7b8d556"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:a4fa4fc04dff799089689f4fd502ce7d59de529fc2f40a2c8836886c03e0175a"}, + {file = "pydantic_core-2.23.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:0a7df63886be5e270da67e0966cf4afbae86069501d35c8c1b3b6c168f42cb36"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dcedcd19a557e182628afa1d553c3895a9f825b936415d0dbd3cd0bbcfd29b4b"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5f54b118ce5de9ac21c363d9b3caa6c800341e8c47a508787e5868c6b79c9323"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:86d2f57d3e1379a9525c5ab067b27dbb8a0642fb5d454e17a9ac434f9ce523e3"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:de6d1d1b9e5101508cb37ab0d972357cac5235f5c6533d1071964c47139257df"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1278e0d324f6908e872730c9102b0112477a7f7cf88b308e4fc36ce1bdb6d58c"}, + {file = "pydantic_core-2.23.4-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9a6b5099eeec78827553827f4c6b8615978bb4b6a88e5d9b93eddf8bb6790f55"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e55541f756f9b3ee346b840103f32779c695a19826a4c442b7954550a0972040"}, + {file = "pydantic_core-2.23.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a5c7ba8ffb6d6f8f2ab08743be203654bb1aaa8c9dcb09f82ddd34eadb695605"}, + {file = "pydantic_core-2.23.4-cp39-none-win32.whl", hash = "sha256:37b0fe330e4a58d3c58b24d91d1eb102aeec675a3db4c292ec3928ecd892a9a6"}, + {file = "pydantic_core-2.23.4-cp39-none-win_amd64.whl", hash = "sha256:1498bec4c05c9c787bde9125cfdcc63a41004ff167f495063191b863399b1a29"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f455ee30a9d61d3e1a15abd5068827773d6e4dc513e795f380cdd59932c782d5"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:1e90d2e3bd2c3863d48525d297cd143fe541be8bbf6f579504b9712cb6b643ec"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e203fdf807ac7e12ab59ca2bfcabb38c7cf0b33c41efeb00f8e5da1d86af480"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e08277a400de01bc72436a0ccd02bdf596631411f592ad985dcee21445bd0068"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f220b0eea5965dec25480b6333c788fb72ce5f9129e8759ef876a1d805d00801"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:d06b0c8da4f16d1d1e352134427cb194a0a6e19ad5db9161bf32b2113409e728"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ba1a0996f6c2773bd83e63f18914c1de3c9dd26d55f4ac302a7efe93fb8e7433"}, + {file = "pydantic_core-2.23.4-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:9a5bce9d23aac8f0cf0836ecfc033896aa8443b501c58d0602dbfd5bd5b37753"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:78ddaaa81421a29574a682b3179d4cf9e6d405a09b99d93ddcf7e5239c742e21"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:883a91b5dd7d26492ff2f04f40fbb652de40fcc0afe07e8129e8ae779c2110eb"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:88ad334a15b32a791ea935af224b9de1bf99bcd62fabf745d5f3442199d86d59"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:233710f069d251feb12a56da21e14cca67994eab08362207785cf8c598e74577"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:19442362866a753485ba5e4be408964644dd6a09123d9416c54cd49171f50744"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:624e278a7d29b6445e4e813af92af37820fafb6dcc55c012c834f9e26f9aaaef"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f5ef8f42bec47f21d07668a043f077d507e5bf4e668d5c6dfe6aaba89de1a5b8"}, + {file = "pydantic_core-2.23.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:aea443fffa9fbe3af1a9ba721a87f926fe548d32cab71d188a6ede77d0ff244e"}, + {file = "pydantic_core-2.23.4.tar.gz", hash = "sha256:2584f7cf844ac4d970fba483a717dbe10c1c1c96a969bf65d61ffe94df1b2863"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pyyaml" +version = "6.0.2" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "typing-extensions" +version = "4.12.2" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.12.2-py3-none-any.whl", hash = "sha256:04e5ca0351e0f3f85c6853954072df659d0d13fac324d0072316b67d7794700d"}, + {file = "typing_extensions-4.12.2.tar.gz", hash = "sha256:1a7ead55c7e559dd4dee8856e3a88b41225abfe1ce8df57b7c13915fe121ffb8"}, +] [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "53f2eabc9c26446fbcc00d348c47878e118afc2054778c3c803a0a8028af27d9" +content-hash = "6d87db38fd2707e51dba4f9b2cd25b3525732047a2010cc4486c8279c4fa9d7f" diff --git a/solution/pricing_rules.py b/solution/pricing_rules.py new file mode 100644 index 0000000..6803a08 --- /dev/null +++ b/solution/pricing_rules.py @@ -0,0 +1,126 @@ +from typing import List +from models import Item + + +class NForMDeal: + + def __init__(self, *, name: str, sku: str, purchase_number_required: str, number_to_pay_for: int) -> None: + self.name = name + self.sku = sku + self.purchase_number_required = purchase_number_required + self.number_to_pay_for = number_to_pay_for + + def qualifies(self, items: List[Item]) -> bool: + self.matching_items = [i for i in items if i.sku == self.sku] + self.n_matching_items = len(self.matching_items) + + return self.n_matching_items >= self.purchase_number_required + + def apply(self, items: List[Item]) -> List[Item]: + # return a list of items with the price updated to 0 if the rule applies + n_free_items = 0 + if self.qualifies(items=items): + print("Qualifies for NForMDeal") + n_free_items = self.purchase_number_required // self.number_to_pay_for + + k = 0 + for ix, item in enumerate(items): + if item.sku == self.sku: + items[ix] = Item( + sku=item.sku, + name=item.name, + price=0.0, + currency=item.currency + ) + + k += 1 + if k == n_free_items: + break + + return items + + +class BulkDiscountDeal: + + def __init__(self, *, name: str, sku: str, purchase_number_required: str, new_item_price: float) -> None: + self.name = name + self.sku = sku + self.purchase_number_required = purchase_number_required + self.new_item_price = new_item_price + + def qualifies(self, items: List[Item]) -> bool: + self.matching_items = [i for i in items if i.sku == self.sku] + self.n_matching_items = len(self.matching_items) + + return self.n_matching_items > self.purchase_number_required + + def apply(self, items: List[Item]) -> List[Item]: + # return a list of items with the price updated to 0 if the rule applies + n_repriced_items = 0 + if self.qualifies(items=items): + print("Qualifies for Bulk Discount Deal") + n_repriced_items = self.n_matching_items + + k = 0 + for ix, item in enumerate(items): + if item.sku == self.sku: + items[ix] = Item( + sku=item.sku, + name=item.name, + price=self.new_item_price, + currency=item.currency + ) + + k += 1 + if k == n_repriced_items: + break + + return items + + +class FreeItemDeal: + + def __init__(self, *, name: str, sku: str, purchase_number_required: str, item_sku: str, n_free_items: int) -> None: + self.name = name + self.sku = sku + self.purchase_number_required = purchase_number_required + self.item_sku = item_sku + self.n_free_items = n_free_items + + def qualifies(self, items: List[Item]) -> bool: + self.matching_items = [i for i in items if i.sku == self.sku] + self.n_matching_items = len(self.matching_items) + + return self.n_matching_items >= self.purchase_number_required + + def apply(self, items: List[Item]) -> List[Item]: + # add the free item(s) with price = 0 + if self.qualifies(items=items): + print("Qualifies for Free Item Deal") + # if item sku is already in items, make it free + # the deal says we get n_free_items for every purchase_number_required + number_for_free = (self.n_matching_items // self.purchase_number_required)*self.n_free_items + # if there are any of the free items already in the list of items, make these free first + for i, item in enumerate(items): + if item.sku == self.item_sku: + items[i] = Item( + sku=item.sku, + name=item.name, + price=0.0, + currency=item.currency + ) + number_for_free -= 1 + if number_for_free <= 0: + break + + # if number_for_free still more than 0, add a free one + while number_for_free >= 0: + items.append(Item( + sku=self.item_sku, + name="FreeItem", + price=0.0, + currency="dollar" + )) + number_for_free -= 1 + + return items diff --git a/solution/pyproject.toml b/solution/pyproject.toml index a5ad666..095a596 100644 --- a/solution/pyproject.toml +++ b/solution/pyproject.toml @@ -8,6 +8,8 @@ package-mode = false [tool.poetry.dependencies] python = "^3.10" +pydantic = "^2.9.2" +pyyaml = "^6.0.2" [build-system] diff --git a/solution/specials.yaml b/solution/specials.yaml new file mode 100644 index 0000000..ea1e6c3 --- /dev/null +++ b/solution/specials.yaml @@ -0,0 +1,16 @@ +FreeItemDeals: +- name: FreeVGAWithMacBook + sku: mbp + item_sku: vga + purchase_number_required: 1 + n_free_items: 1 +BulkDiscountDeals: +- name: iPadBulkDiscount + sku: ipd + purchase_number_required: 4 + new_item_price: 499.99 +NForMDeals: +- name: Apple3for2 + sku: atv + purchase_number_required: 3 + number_to_pay_for: 2 \ No newline at end of file diff --git a/solution/test.py b/solution/test.py new file mode 100644 index 0000000..b30b67a --- /dev/null +++ b/solution/test.py @@ -0,0 +1,55 @@ +import yaml +from pricing_rules import ( + BulkDiscountDeal, + NForMDeal, + FreeItemDeal +) + +from checkout import Checkout + +if __name__ == "__main__": + # load pricing rules + with open("specials.yaml") as f: + deals = yaml.safe_load(f) + + free_item_deals = deals["FreeItemDeals"] + bulk_discount_deals = deals["BulkDiscountDeals"] + n_for_m_deals = deals["NForMDeals"] + + pricing_rules = list() + + for deal in free_item_deals: + pricing_rules.append( + FreeItemDeal( + **deal + ) + ) + + for deal in bulk_discount_deals: + pricing_rules.append( + BulkDiscountDeal( + **deal + ) + ) + + for deal in n_for_m_deals: + pricing_rules.append( + NForMDeal( + **deal + ) + ) + + # construct checkout object + with open("catalogue.yaml") as f: + catalogue = yaml.safe_load(f) + + co = Checkout( + pricing_rules=pricing_rules, + catalogue=catalogue + ) + + # scan things + items_to_scan = ["atv", "ipd", "ipd", "atv", "ipd", "ipd", "ipd"] + for sku in items_to_scan: + co.scan(sku) + print(co.total()) \ No newline at end of file From b87d1c6b95e7641a3ac33142049ce309c37d64ef Mon Sep 17 00:00:00 2001 From: Richa Date: Mon, 23 Sep 2024 08:45:13 +0100 Subject: [PATCH 04/10] how to run example --- solution/README.md | 13 +++++- solution/checkout.py | 1 - solution/{test.py => example.py} | 19 ++++++-- solution/models.py | 10 +++- solution/pricing_rules.py | 80 +++++++++++++------------------- 5 files changed, 70 insertions(+), 53 deletions(-) rename solution/{test.py => example.py} (67%) diff --git a/solution/README.md b/solution/README.md index eb398b1..77e3920 100644 --- a/solution/README.md +++ b/solution/README.md @@ -24,7 +24,18 @@ poetry show ``` All installed requirements will be returned in blue. If any are not installed they will appear as red. Try running `poetry install` again if this happens, or installing the missing packages individually. -## Running the App +## Running the Example script +From the command line, move to the `/solution` folder: +``` +cd solution +``` + +Then run: +``` +poetry run python example.py +``` + +Follow the instructions to enter the skus of items that need scanning and hit enter to print the total. ## Running Tests - To run all tests, run: diff --git a/solution/checkout.py b/solution/checkout.py index ff5d8de..469a034 100644 --- a/solution/checkout.py +++ b/solution/checkout.py @@ -20,7 +20,6 @@ def total(self): self.items_with_deals = self.scanned_items.copy() for rule in self.pricing_rules: self.items_with_deals = rule.apply(self.items_with_deals) - breakpoint() return sum(item.price for item in self.items_with_deals) def receipt(self): diff --git a/solution/test.py b/solution/example.py similarity index 67% rename from solution/test.py rename to solution/example.py index b30b67a..1c51a16 100644 --- a/solution/test.py +++ b/solution/example.py @@ -1,12 +1,19 @@ +import json +import logging + import yaml + from pricing_rules import ( BulkDiscountDeal, NForMDeal, FreeItemDeal ) - from checkout import Checkout +logging.basicConfig( + level=logging.INFO +) + if __name__ == "__main__": # load pricing rules with open("specials.yaml") as f: @@ -49,7 +56,13 @@ ) # scan things - items_to_scan = ["atv", "ipd", "ipd", "atv", "ipd", "ipd", "ipd"] + items_to_scan = input("Enter the skus of the items you would like to be scanned, separated by a comma: ") + items_to_scan = items_to_scan.replace(" ", "") + items_to_scan = items_to_scan.split(",") for sku in items_to_scan: co.scan(sku) - print(co.total()) \ No newline at end of file + print(f"Total: ${co.total()}") + + view_receipt = input("Do you want to see an itemized receipt with discounts applied? (y/n) ") + if view_receipt.lower() == "y": + print(json.dumps([i.model_dump() for i in co.receipt()], indent=2)) \ No newline at end of file diff --git a/solution/models.py b/solution/models.py index bfeafa6..be5f8ca 100644 --- a/solution/models.py +++ b/solution/models.py @@ -1,4 +1,4 @@ -from typing import List, Protocol +from typing import Any, List, Protocol from pydantic import BaseModel class Item(BaseModel): @@ -7,6 +7,14 @@ class Item(BaseModel): price: float currency: str + def update(self, attr_to_update: str, new_value: Any): + current = self.model_dump() + current[attr_to_update] = new_value + + new = Item(**current) + + return new + class PricingRule(Protocol): def __init__(self, *args, **kwargs) -> None: diff --git a/solution/pricing_rules.py b/solution/pricing_rules.py index 6803a08..690b88e 100644 --- a/solution/pricing_rules.py +++ b/solution/pricing_rules.py @@ -1,6 +1,15 @@ +import logging from typing import List + from models import Item +logger = logging.getLogger(__name__) + +def count_matching_items(items: List[Item], sku_to_match: str): + matching_items = [i for i in items if i.sku == sku_to_match] + n_matching_items = len(matching_items) + + return n_matching_items class NForMDeal: @@ -9,32 +18,24 @@ def __init__(self, *, name: str, sku: str, purchase_number_required: str, number self.sku = sku self.purchase_number_required = purchase_number_required self.number_to_pay_for = number_to_pay_for - - def qualifies(self, items: List[Item]) -> bool: - self.matching_items = [i for i in items if i.sku == self.sku] - self.n_matching_items = len(self.matching_items) - - return self.n_matching_items >= self.purchase_number_required def apply(self, items: List[Item]) -> List[Item]: # return a list of items with the price updated to 0 if the rule applies n_free_items = 0 - if self.qualifies(items=items): - print("Qualifies for NForMDeal") + self.n_matching_items = count_matching_items(items=items, sku_to_match=self.sku) + if self.n_matching_items >= self.purchase_number_required: + logger.info("Qualifies for NForMDeal") n_free_items = self.purchase_number_required // self.number_to_pay_for - k = 0 for ix, item in enumerate(items): if item.sku == self.sku: - items[ix] = Item( - sku=item.sku, - name=item.name, - price=0.0, - currency=item.currency + items[ix] = items[ix].update( + attr_to_update="price", + new_value=0.0 ) - k += 1 - if k == n_free_items: + n_free_items -= 1 + if n_free_items <= 0: break return items @@ -48,31 +49,23 @@ def __init__(self, *, name: str, sku: str, purchase_number_required: str, new_it self.purchase_number_required = purchase_number_required self.new_item_price = new_item_price - def qualifies(self, items: List[Item]) -> bool: - self.matching_items = [i for i in items if i.sku == self.sku] - self.n_matching_items = len(self.matching_items) - - return self.n_matching_items > self.purchase_number_required - def apply(self, items: List[Item]) -> List[Item]: # return a list of items with the price updated to 0 if the rule applies n_repriced_items = 0 - if self.qualifies(items=items): - print("Qualifies for Bulk Discount Deal") + self.n_matching_items = count_matching_items(items=items, sku_to_match=self.sku) + if self.n_matching_items > self.purchase_number_required: + logger.info("Qualifies for Bulk Discount Deal") n_repriced_items = self.n_matching_items - k = 0 for ix, item in enumerate(items): if item.sku == self.sku: - items[ix] = Item( - sku=item.sku, - name=item.name, - price=self.new_item_price, - currency=item.currency + items[ix] = items[ix].update( + attr_to_update="price", + new_value=self.new_item_price ) - k += 1 - if k == n_repriced_items: + n_repriced_items -= 1 + if n_repriced_items <= 0: break return items @@ -87,34 +80,27 @@ def __init__(self, *, name: str, sku: str, purchase_number_required: str, item_s self.item_sku = item_sku self.n_free_items = n_free_items - def qualifies(self, items: List[Item]) -> bool: - self.matching_items = [i for i in items if i.sku == self.sku] - self.n_matching_items = len(self.matching_items) - - return self.n_matching_items >= self.purchase_number_required - def apply(self, items: List[Item]) -> List[Item]: # add the free item(s) with price = 0 - if self.qualifies(items=items): - print("Qualifies for Free Item Deal") + self.n_matching_items = count_matching_items(items=items, sku_to_match=self.sku) + if self.n_matching_items >= self.purchase_number_required: + logger.info("Qualifies for Free Item Deal") # if item sku is already in items, make it free # the deal says we get n_free_items for every purchase_number_required number_for_free = (self.n_matching_items // self.purchase_number_required)*self.n_free_items # if there are any of the free items already in the list of items, make these free first - for i, item in enumerate(items): + for ix, item in enumerate(items): if item.sku == self.item_sku: - items[i] = Item( - sku=item.sku, - name=item.name, - price=0.0, - currency=item.currency + items[ix] = items[ix].update( + attr_to_update="price", + new_value=0.0 ) number_for_free -= 1 if number_for_free <= 0: break # if number_for_free still more than 0, add a free one - while number_for_free >= 0: + while number_for_free > 0: items.append(Item( sku=self.item_sku, name="FreeItem", From f0f133acaac3c611c351d830f94bf4a3aa481a52 Mon Sep 17 00:00:00 2001 From: Richa Date: Mon, 23 Sep 2024 08:47:23 +0100 Subject: [PATCH 05/10] format --- solution/Makefile | 5 +- solution/checkout.py | 5 +- solution/example.py | 45 ++++--------- solution/models.py | 13 ++-- solution/poetry.lock | 133 +++++++++++++++++++++++++++++++++++++- solution/pricing_rules.py | 66 ++++++++++++------- solution/pyproject.toml | 1 + 7 files changed, 202 insertions(+), 66 deletions(-) diff --git a/solution/Makefile b/solution/Makefile index 5e08975..d72797d 100644 --- a/solution/Makefile +++ b/solution/Makefile @@ -2,4 +2,7 @@ test: poetry run pytest coverage: - poetry run coverage \ No newline at end of file + poetry run coverage + +format: + poetry run black . \ No newline at end of file diff --git a/solution/checkout.py b/solution/checkout.py index 469a034..2d26d29 100644 --- a/solution/checkout.py +++ b/solution/checkout.py @@ -4,18 +4,19 @@ logger = logging.getLogger(__name__) + class Checkout: def __init__(self, pricing_rules: List[PricingRule], catalogue: Dict[str, Item]): self.pricing_rules = pricing_rules self.scanned_items = list() self.catalogue = catalogue - + def scan(self, item_sku: str) -> None: item_attrs = self.catalogue.get(item_sku) item = Item(**item_attrs) self.scanned_items.append(item) - + def total(self): self.items_with_deals = self.scanned_items.copy() for rule in self.pricing_rules: diff --git a/solution/example.py b/solution/example.py index 1c51a16..1dbd289 100644 --- a/solution/example.py +++ b/solution/example.py @@ -3,22 +3,16 @@ import yaml -from pricing_rules import ( - BulkDiscountDeal, - NForMDeal, - FreeItemDeal -) +from pricing_rules import BulkDiscountDeal, NForMDeal, FreeItemDeal from checkout import Checkout -logging.basicConfig( - level=logging.INFO -) +logging.basicConfig(level=logging.INFO) if __name__ == "__main__": # load pricing rules with open("specials.yaml") as f: deals = yaml.safe_load(f) - + free_item_deals = deals["FreeItemDeals"] bulk_discount_deals = deals["BulkDiscountDeals"] n_for_m_deals = deals["NForMDeals"] @@ -26,43 +20,32 @@ pricing_rules = list() for deal in free_item_deals: - pricing_rules.append( - FreeItemDeal( - **deal - ) - ) + pricing_rules.append(FreeItemDeal(**deal)) for deal in bulk_discount_deals: - pricing_rules.append( - BulkDiscountDeal( - **deal - ) - ) + pricing_rules.append(BulkDiscountDeal(**deal)) for deal in n_for_m_deals: - pricing_rules.append( - NForMDeal( - **deal - ) - ) + pricing_rules.append(NForMDeal(**deal)) # construct checkout object with open("catalogue.yaml") as f: catalogue = yaml.safe_load(f) - co = Checkout( - pricing_rules=pricing_rules, - catalogue=catalogue - ) + co = Checkout(pricing_rules=pricing_rules, catalogue=catalogue) # scan things - items_to_scan = input("Enter the skus of the items you would like to be scanned, separated by a comma: ") + items_to_scan = input( + "Enter the skus of the items you would like to be scanned, separated by a comma: " + ) items_to_scan = items_to_scan.replace(" ", "") items_to_scan = items_to_scan.split(",") for sku in items_to_scan: co.scan(sku) print(f"Total: ${co.total()}") - view_receipt = input("Do you want to see an itemized receipt with discounts applied? (y/n) ") + view_receipt = input( + "Do you want to see an itemized receipt with discounts applied? (y/n) " + ) if view_receipt.lower() == "y": - print(json.dumps([i.model_dump() for i in co.receipt()], indent=2)) \ No newline at end of file + print(json.dumps([i.model_dump() for i in co.receipt()], indent=2)) diff --git a/solution/models.py b/solution/models.py index be5f8ca..7182fd1 100644 --- a/solution/models.py +++ b/solution/models.py @@ -1,6 +1,7 @@ from typing import Any, List, Protocol from pydantic import BaseModel + class Item(BaseModel): sku: str name: str @@ -10,18 +11,16 @@ class Item(BaseModel): def update(self, attr_to_update: str, new_value: Any): current = self.model_dump() current[attr_to_update] = new_value - + new = Item(**current) return new + class PricingRule(Protocol): - def __init__(self, *args, **kwargs) -> None: - ... + def __init__(self, *args, **kwargs) -> None: ... - def qualifies(self, items: List[Item]) -> bool: - ... + def qualifies(self, items: List[Item]) -> bool: ... - def apply(self, items: List[Item]) -> List[Item]: - ... \ No newline at end of file + def apply(self, items: List[Item]) -> List[Item]: ... diff --git a/solution/poetry.lock b/solution/poetry.lock index da703dd..0ee1074 100644 --- a/solution/poetry.lock +++ b/solution/poetry.lock @@ -11,6 +11,126 @@ files = [ {file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"}, ] +[[package]] +name = "black" +version = "24.8.0" +description = "The uncompromising code formatter." +optional = false +python-versions = ">=3.8" +files = [ + {file = "black-24.8.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:09cdeb74d494ec023ded657f7092ba518e8cf78fa8386155e4a03fdcc44679e6"}, + {file = "black-24.8.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:81c6742da39f33b08e791da38410f32e27d632260e599df7245cccee2064afeb"}, + {file = "black-24.8.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:707a1ca89221bc8a1a64fb5e15ef39cd755633daa672a9db7498d1c19de66a42"}, + {file = "black-24.8.0-cp310-cp310-win_amd64.whl", hash = "sha256:d6417535d99c37cee4091a2f24eb2b6d5ec42b144d50f1f2e436d9fe1916fe1a"}, + {file = "black-24.8.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fb6e2c0b86bbd43dee042e48059c9ad7830abd5c94b0bc518c0eeec57c3eddc1"}, + {file = "black-24.8.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:837fd281f1908d0076844bc2b801ad2d369c78c45cf800cad7b61686051041af"}, + {file = "black-24.8.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:62e8730977f0b77998029da7971fa896ceefa2c4c4933fcd593fa599ecbf97a4"}, + {file = "black-24.8.0-cp311-cp311-win_amd64.whl", hash = "sha256:72901b4913cbac8972ad911dc4098d5753704d1f3c56e44ae8dce99eecb0e3af"}, + {file = "black-24.8.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:7c046c1d1eeb7aea9335da62472481d3bbf3fd986e093cffd35f4385c94ae368"}, + {file = "black-24.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:649f6d84ccbae73ab767e206772cc2d7a393a001070a4c814a546afd0d423aed"}, + {file = "black-24.8.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2b59b250fdba5f9a9cd9d0ece6e6d993d91ce877d121d161e4698af3eb9c1018"}, + {file = "black-24.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:6e55d30d44bed36593c3163b9bc63bf58b3b30e4611e4d88a0c3c239930ed5b2"}, + {file = "black-24.8.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:505289f17ceda596658ae81b61ebbe2d9b25aa78067035184ed0a9d855d18afd"}, + {file = "black-24.8.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:b19c9ad992c7883ad84c9b22aaa73562a16b819c1d8db7a1a1a49fb7ec13c7d2"}, + {file = "black-24.8.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1f13f7f386f86f8121d76599114bb8c17b69d962137fc70efe56137727c7047e"}, + {file = "black-24.8.0-cp38-cp38-win_amd64.whl", hash = "sha256:f490dbd59680d809ca31efdae20e634f3fae27fba3ce0ba3208333b713bc3920"}, + {file = "black-24.8.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eab4dd44ce80dea27dc69db40dab62d4ca96112f87996bca68cd75639aeb2e4c"}, + {file = "black-24.8.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3c4285573d4897a7610054af5a890bde7c65cb466040c5f0c8b732812d7f0e5e"}, + {file = "black-24.8.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e84e33b37be070ba135176c123ae52a51f82306def9f7d063ee302ecab2cf47"}, + {file = "black-24.8.0-cp39-cp39-win_amd64.whl", hash = "sha256:73bbf84ed136e45d451a260c6b73ed674652f90a2b3211d6a35e78054563a9bb"}, + {file = "black-24.8.0-py3-none-any.whl", hash = "sha256:972085c618ee94f402da1af548a4f218c754ea7e5dc70acb168bfaca4c2542ed"}, + {file = "black-24.8.0.tar.gz", hash = "sha256:2500945420b6784c38b9ee885af039f5e7471ef284ab03fa35ecdde4688cd83f"}, +] + +[package.dependencies] +click = ">=8.0.0" +mypy-extensions = ">=0.4.3" +packaging = ">=22.0" +pathspec = ">=0.9.0" +platformdirs = ">=2" +tomli = {version = ">=1.1.0", markers = "python_version < \"3.11\""} +typing-extensions = {version = ">=4.0.1", markers = "python_version < \"3.11\""} + +[package.extras] +colorama = ["colorama (>=0.4.3)"] +d = ["aiohttp (>=3.7.4)", "aiohttp (>=3.7.4,!=3.9.0)"] +jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] +uvloop = ["uvloop (>=0.15.2)"] + +[[package]] +name = "click" +version = "8.1.7" +description = "Composable command line interface toolkit" +optional = false +python-versions = ">=3.7" +files = [ + {file = "click-8.1.7-py3-none-any.whl", hash = "sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28"}, + {file = "click-8.1.7.tar.gz", hash = "sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "platform_system == \"Windows\""} + +[[package]] +name = "colorama" +version = "0.4.6" +description = "Cross-platform colored terminal text." +optional = false +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "mypy-extensions" +version = "1.0.0" +description = "Type system extensions for programs checked with the mypy type checker." +optional = false +python-versions = ">=3.5" +files = [ + {file = "mypy_extensions-1.0.0-py3-none-any.whl", hash = "sha256:4392f6c0eb8a5668a69e23d168ffa70f0be9ccfd32b5cc2d26a34ae5b844552d"}, + {file = "mypy_extensions-1.0.0.tar.gz", hash = "sha256:75dbf8955dc00442a438fc4d0666508a9a97b6bd41aa2f0ffe9d2f2725af0782"}, +] + +[[package]] +name = "packaging" +version = "24.1" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.8" +files = [ + {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, + {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +description = "Utility library for gitignore style pattern matching of file paths." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08"}, + {file = "pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712"}, +] + +[[package]] +name = "platformdirs" +version = "4.3.6" +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.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, + {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, +] + +[package.extras] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-autodoc-typehints (>=2.4)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.11.2)"] + [[package]] name = "pydantic" version = "2.9.2" @@ -197,6 +317,17 @@ files = [ {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, ] +[[package]] +name = "tomli" +version = "2.0.1" +description = "A lil' TOML parser" +optional = false +python-versions = ">=3.7" +files = [ + {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, + {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, +] + [[package]] name = "typing-extensions" version = "4.12.2" @@ -211,4 +342,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "6d87db38fd2707e51dba4f9b2cd25b3525732047a2010cc4486c8279c4fa9d7f" +content-hash = "6ead5513ca7558193648b8917b1f8d1fafb9ebe21fbf32775468d63f4206ddc1" diff --git a/solution/pricing_rules.py b/solution/pricing_rules.py index 690b88e..06b9bbd 100644 --- a/solution/pricing_rules.py +++ b/solution/pricing_rules.py @@ -5,15 +5,24 @@ logger = logging.getLogger(__name__) + def count_matching_items(items: List[Item], sku_to_match: str): matching_items = [i for i in items if i.sku == sku_to_match] n_matching_items = len(matching_items) return n_matching_items + class NForMDeal: - def __init__(self, *, name: str, sku: str, purchase_number_required: str, number_to_pay_for: int) -> None: + def __init__( + self, + *, + name: str, + sku: str, + purchase_number_required: str, + number_to_pay_for: int + ) -> None: self.name = name self.sku = sku self.purchase_number_required = purchase_number_required @@ -26,24 +35,28 @@ def apply(self, items: List[Item]) -> List[Item]: if self.n_matching_items >= self.purchase_number_required: logger.info("Qualifies for NForMDeal") n_free_items = self.purchase_number_required // self.number_to_pay_for - + for ix, item in enumerate(items): if item.sku == self.sku: - items[ix] = items[ix].update( - attr_to_update="price", - new_value=0.0 - ) + items[ix] = items[ix].update(attr_to_update="price", new_value=0.0) n_free_items -= 1 if n_free_items <= 0: break - + return items class BulkDiscountDeal: - def __init__(self, *, name: str, sku: str, purchase_number_required: str, new_item_price: float) -> None: + def __init__( + self, + *, + name: str, + sku: str, + purchase_number_required: str, + new_item_price: float + ) -> None: self.name = name self.sku = sku self.purchase_number_required = purchase_number_required @@ -56,12 +69,11 @@ def apply(self, items: List[Item]) -> List[Item]: if self.n_matching_items > self.purchase_number_required: logger.info("Qualifies for Bulk Discount Deal") n_repriced_items = self.n_matching_items - + for ix, item in enumerate(items): if item.sku == self.sku: items[ix] = items[ix].update( - attr_to_update="price", - new_value=self.new_item_price + attr_to_update="price", new_value=self.new_item_price ) n_repriced_items -= 1 @@ -69,11 +81,19 @@ def apply(self, items: List[Item]) -> List[Item]: break return items - + class FreeItemDeal: - def __init__(self, *, name: str, sku: str, purchase_number_required: str, item_sku: str, n_free_items: int) -> None: + def __init__( + self, + *, + name: str, + sku: str, + purchase_number_required: str, + item_sku: str, + n_free_items: int + ) -> None: self.name = name self.sku = sku self.purchase_number_required = purchase_number_required @@ -87,26 +107,24 @@ def apply(self, items: List[Item]) -> List[Item]: logger.info("Qualifies for Free Item Deal") # if item sku is already in items, make it free # the deal says we get n_free_items for every purchase_number_required - number_for_free = (self.n_matching_items // self.purchase_number_required)*self.n_free_items + number_for_free = ( + self.n_matching_items // self.purchase_number_required + ) * self.n_free_items # if there are any of the free items already in the list of items, make these free first for ix, item in enumerate(items): if item.sku == self.item_sku: - items[ix] = items[ix].update( - attr_to_update="price", - new_value=0.0 - ) + items[ix] = items[ix].update(attr_to_update="price", new_value=0.0) number_for_free -= 1 if number_for_free <= 0: break # if number_for_free still more than 0, add a free one while number_for_free > 0: - items.append(Item( - sku=self.item_sku, - name="FreeItem", - price=0.0, - currency="dollar" - )) + items.append( + Item( + sku=self.item_sku, name="FreeItem", price=0.0, currency="dollar" + ) + ) number_for_free -= 1 return items diff --git a/solution/pyproject.toml b/solution/pyproject.toml index 095a596..51dfe15 100644 --- a/solution/pyproject.toml +++ b/solution/pyproject.toml @@ -10,6 +10,7 @@ package-mode = false python = "^3.10" pydantic = "^2.9.2" pyyaml = "^6.0.2" +black = "^24.8.0" [build-system] From 3b0decbdec2c03ec2d5c0dd4ddccfa65fbdc1f4b Mon Sep 17 00:00:00 2001 From: Richa Date: Mon, 23 Sep 2024 08:58:02 +0100 Subject: [PATCH 06/10] docstrings --- solution/checkout.py | 25 +++++++++++++++++++++++-- solution/models.py | 11 +++++++++-- solution/pricing_rules.py | 33 ++++++++++++++++++++++++++++++--- 3 files changed, 62 insertions(+), 7 deletions(-) diff --git a/solution/checkout.py b/solution/checkout.py index 2d26d29..b5185cc 100644 --- a/solution/checkout.py +++ b/solution/checkout.py @@ -8,20 +8,41 @@ class Checkout: def __init__(self, pricing_rules: List[PricingRule], catalogue: Dict[str, Item]): + """Creates Checkout object. + + Args: + pricing_rules (List[PricingRule]): A list of rules that specify logic relating to offers. + catalogue (Dict[str, Item]): All items available, indexed by their unique sku. + """ self.pricing_rules = pricing_rules self.scanned_items = list() self.catalogue = catalogue def scan(self, item_sku: str) -> None: + """Adds an item to the list of items being purchased + + Args: + item_sku (str): Unique identifier of the item. + """ item_attrs = self.catalogue.get(item_sku) item = Item(**item_attrs) self.scanned_items.append(item) - def total(self): + def total(self) -> float: + """Calculates the total sum of all scanned items, with any discounts applied. + + Returns: + float: The total cost of the scanned items. + """ self.items_with_deals = self.scanned_items.copy() for rule in self.pricing_rules: self.items_with_deals = rule.apply(self.items_with_deals) return sum(item.price for item in self.items_with_deals) - def receipt(self): + def receipt(self) -> List[Item]: + """Returns the list of all scanned items, with any discounts applied. + + Returns: + List[Item]: _description_ + """ return self.items_with_deals diff --git a/solution/models.py b/solution/models.py index 7182fd1..32597dd 100644 --- a/solution/models.py +++ b/solution/models.py @@ -9,6 +9,15 @@ class Item(BaseModel): currency: str def update(self, attr_to_update: str, new_value: Any): + """Returns a new version of the item with the relevant field updated. + + Args: + attr_to_update (str): The field to update + new_value (Any): The updated value of the field. + + Returns: + Item: A new version of the Item. + """ current = self.model_dump() current[attr_to_update] = new_value @@ -21,6 +30,4 @@ class PricingRule(Protocol): def __init__(self, *args, **kwargs) -> None: ... - def qualifies(self, items: List[Item]) -> bool: ... - def apply(self, items: List[Item]) -> List[Item]: ... diff --git a/solution/pricing_rules.py b/solution/pricing_rules.py index 06b9bbd..4971e77 100644 --- a/solution/pricing_rules.py +++ b/solution/pricing_rules.py @@ -14,7 +14,8 @@ def count_matching_items(items: List[Item], sku_to_match: str): class NForMDeal: - + """Logic to apply the N for M deal e.g. 3 for 2. + """ def __init__( self, *, @@ -29,6 +30,14 @@ def __init__( self.number_to_pay_for = number_to_pay_for def apply(self, items: List[Item]) -> List[Item]: + """Applies the logic for the N for M Deal + + Args: + items (List[Item]): All scanned items + + Returns: + List[Item]: All scanned items with prices updated for any items where this deal applies. + """ # return a list of items with the price updated to 0 if the rule applies n_free_items = 0 self.n_matching_items = count_matching_items(items=items, sku_to_match=self.sku) @@ -48,7 +57,8 @@ def apply(self, items: List[Item]) -> List[Item]: class BulkDiscountDeal: - + """Logic for the Bulk Discount Deal e.g. buy more than 4 of an item and have the price reduced for all of these items. + """ def __init__( self, *, @@ -63,6 +73,14 @@ def __init__( self.new_item_price = new_item_price def apply(self, items: List[Item]) -> List[Item]: + """Applies the logic for the Bulk Discount deal. + + Args: + items (List[Item]): All scanned items + + Returns: + List[Item]: All scanned items with prices updated for any items where this deal applies. + """ # return a list of items with the price updated to 0 if the rule applies n_repriced_items = 0 self.n_matching_items = count_matching_items(items=items, sku_to_match=self.sku) @@ -84,7 +102,8 @@ def apply(self, items: List[Item]) -> List[Item]: class FreeItemDeal: - + """Logic for the Free Item Deal e.g. buy an item and receive a free item with it. + """ def __init__( self, *, @@ -101,6 +120,14 @@ def __init__( self.n_free_items = n_free_items def apply(self, items: List[Item]) -> List[Item]: + """Applies the logic for the Free Item Deal. + + Args: + items (List[Item]): All scanned items + + Returns: + List[Item]: All scanned items with prices updated for any items where this deal applies. + """ # add the free item(s) with price = 0 self.n_matching_items = count_matching_items(items=items, sku_to_match=self.sku) if self.n_matching_items >= self.purchase_number_required: From 33ea45e1b585fab0df6432567738f17a86f2bd70 Mon Sep 17 00:00:00 2001 From: Richa Date: Mon, 23 Sep 2024 09:03:32 +0100 Subject: [PATCH 07/10] restructure --- solution/__init__.py | 0 solution/{ => data}/catalogue.yaml | 0 solution/{ => data}/specials.yaml | 0 solution/example.py | 8 ++-- solution/poetry.lock | 64 ++++++++++++++++++++++++++++- solution/pyproject.toml | 1 + solution/src/__init__.py | 0 solution/{ => src}/checkout.py | 2 +- solution/{ => src}/models.py | 0 solution/{ => src}/pricing_rules.py | 2 +- solution/tests/test.py | 0 11 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 solution/__init__.py rename solution/{ => data}/catalogue.yaml (100%) rename solution/{ => data}/specials.yaml (100%) create mode 100644 solution/src/__init__.py rename solution/{ => src}/checkout.py (97%) rename solution/{ => src}/models.py (100%) rename solution/{ => src}/pricing_rules.py (99%) create mode 100644 solution/tests/test.py diff --git a/solution/__init__.py b/solution/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/catalogue.yaml b/solution/data/catalogue.yaml similarity index 100% rename from solution/catalogue.yaml rename to solution/data/catalogue.yaml diff --git a/solution/specials.yaml b/solution/data/specials.yaml similarity index 100% rename from solution/specials.yaml rename to solution/data/specials.yaml diff --git a/solution/example.py b/solution/example.py index 1dbd289..1dacd78 100644 --- a/solution/example.py +++ b/solution/example.py @@ -3,14 +3,14 @@ import yaml -from pricing_rules import BulkDiscountDeal, NForMDeal, FreeItemDeal -from checkout import Checkout +from src.pricing_rules import BulkDiscountDeal, NForMDeal, FreeItemDeal +from src.checkout import Checkout logging.basicConfig(level=logging.INFO) if __name__ == "__main__": # load pricing rules - with open("specials.yaml") as f: + with open("data/specials.yaml") as f: deals = yaml.safe_load(f) free_item_deals = deals["FreeItemDeals"] @@ -29,7 +29,7 @@ pricing_rules.append(NForMDeal(**deal)) # construct checkout object - with open("catalogue.yaml") as f: + with open("data/catalogue.yaml") as f: catalogue = yaml.safe_load(f) co = Checkout(pricing_rules=pricing_rules, catalogue=catalogue) diff --git a/solution/poetry.lock b/solution/poetry.lock index 0ee1074..5dcda03 100644 --- a/solution/poetry.lock +++ b/solution/poetry.lock @@ -82,6 +82,31 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "exceptiongroup" +version = "1.2.2" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, + {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "iniconfig" +version = "2.0.0" +description = "brain-dead simple config-ini parsing" +optional = false +python-versions = ">=3.7" +files = [ + {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, + {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, +] + [[package]] name = "mypy-extensions" version = "1.0.0" @@ -131,6 +156,21 @@ docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.0.2)", "sphinx-a test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.2)", "pytest-cov (>=5)", "pytest-mock (>=3.14)"] type = ["mypy (>=1.11.2)"] +[[package]] +name = "pluggy" +version = "1.5.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, + {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + [[package]] name = "pydantic" version = "2.9.2" @@ -255,6 +295,28 @@ files = [ [package.dependencies] typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" +[[package]] +name = "pytest" +version = "8.3.3" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.3.3-py3-none-any.whl", hash = "sha256:a6853c7375b2663155079443d2e45de913a911a11d669df02a50814944db57b2"}, + {file = "pytest-8.3.3.tar.gz", hash = "sha256:70b98107bd648308a7952b06e6ca9a50bc660be218d53c257cc1fc94fda10181"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.5,<2" +tomli = {version = ">=1", markers = "python_version < \"3.11\""} + +[package.extras] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -342,4 +404,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "6ead5513ca7558193648b8917b1f8d1fafb9ebe21fbf32775468d63f4206ddc1" +content-hash = "a56d92b1f586be9dc534bde7db7efd33fa6c8fcb52d78c32d96295aef6191140" diff --git a/solution/pyproject.toml b/solution/pyproject.toml index 51dfe15..49da10f 100644 --- a/solution/pyproject.toml +++ b/solution/pyproject.toml @@ -11,6 +11,7 @@ python = "^3.10" pydantic = "^2.9.2" pyyaml = "^6.0.2" black = "^24.8.0" +pytest = "^8.3.3" [build-system] diff --git a/solution/src/__init__.py b/solution/src/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/solution/checkout.py b/solution/src/checkout.py similarity index 97% rename from solution/checkout.py rename to solution/src/checkout.py index b5185cc..d53d4d9 100644 --- a/solution/checkout.py +++ b/solution/src/checkout.py @@ -1,5 +1,5 @@ from typing import List, Dict -from models import PricingRule, Item +from src.models import PricingRule, Item import logging logger = logging.getLogger(__name__) diff --git a/solution/models.py b/solution/src/models.py similarity index 100% rename from solution/models.py rename to solution/src/models.py diff --git a/solution/pricing_rules.py b/solution/src/pricing_rules.py similarity index 99% rename from solution/pricing_rules.py rename to solution/src/pricing_rules.py index 4971e77..2a95934 100644 --- a/solution/pricing_rules.py +++ b/solution/src/pricing_rules.py @@ -1,7 +1,7 @@ import logging from typing import List -from models import Item +from src.models import Item logger = logging.getLogger(__name__) diff --git a/solution/tests/test.py b/solution/tests/test.py new file mode 100644 index 0000000..e69de29 From b51a02552ab3d037b8275b99c1527648c5486264 Mon Sep 17 00:00:00 2001 From: Richa Date: Mon, 23 Sep 2024 11:45:33 +0100 Subject: [PATCH 08/10] tests --- solution/.coverage | Bin 0 -> 53248 bytes solution/Makefile | 2 +- solution/data/catalogue.yaml | 5 +- solution/poetry.lock | 107 ++++++++++++++++++++++++++++++- solution/pyproject.toml | 1 + solution/src/checkout.py | 3 + solution/src/exceptions.py | 2 + solution/src/pricing_rules.py | 21 ++++-- solution/{ => tests}/__init__.py | 0 solution/tests/conftest.py | 56 ++++++++++++++++ solution/tests/test.py | 0 solution/tests/test_checkout.py | 105 ++++++++++++++++++++++++++++++ 12 files changed, 293 insertions(+), 9 deletions(-) create mode 100644 solution/.coverage create mode 100644 solution/src/exceptions.py rename solution/{ => tests}/__init__.py (100%) create mode 100644 solution/tests/conftest.py delete mode 100644 solution/tests/test.py create mode 100644 solution/tests/test_checkout.py diff --git a/solution/.coverage b/solution/.coverage new file mode 100644 index 0000000000000000000000000000000000000000..e755abe3191a835ac83b84667f05b9c6c9ff6bbe GIT binary patch literal 53248 zcmeI)+iM(E90%~Z?QV8+nMz5QG=v-|nuaD>gP^Dn8g0N{q!()ulx}BdPm-}aGuxTj zHW8%VRwxL*_DRG)!3TZz4^SU`@LBrO_CZlW1!r3i5HYzimEEF3!x}Vk=~>9&ZYt_7}*Mas$I+d7K_TlN6k_5 zv@(+aOfk=l9y8}hJ|BH<_{R~q`04N$1*>S&4onb$00bcL{}yPU7%7w|Ce$k*M^>#N zqrkFdkjzKlJh{AdYFV6GdgJJ_NVbXnB|+2tyjT)}e^E4LAXc1)6pmMSY%6lSRS~Vp zEOaZBbsp$w8x3_Fa3PM*OI4>%u_C!jNi+k;wSox!CX2XG^nJFO>@A-MiImGA@%9*z_9iDGhDt6y?wgC1m?SopWCc zEYDt(;heBGOKAI)&-ExNZQ#3=$QKRAi;L-nPUQHWkn7TJMY6ta0p}Pkn+UJFG?dm^ zF3{N|W_qkHolfL!Vy3~<$SagN&(Byp5&6&*InJ}!a%)8XVasju$)Om@fg`_P%zkkr zKP~J-pPOyYWv9G7r!ynxGn&L|dCvKk+&uYnq7!p2E{5mN2`&WHfM3e`%v z9-eQ|_pPeciu`!JN+-X%NVAboo*62Xr>E6x=i`dPJk^>Lg_{e-o=UU1xlh&M{r&3= zx02pU!mX&$5dKQsWH`1+P7c-P?lx1&Fsy(Zn()(rl+!0#+c>k72T^PIwcffG*O*XJ z1EIbk3%1hoY;Gzcm%;Wg$iP~a3+djZTX@nHiK@DF@bKAhTG5)gKkvMmGCq^bmtP(4mvPQQ zb&==!tM6-Zmak33`Yv_){MLK2%v(BHJ|Z{fFIWxgO`4VyP?M!TI2i#12drBZ*sA%h zD6YS}8dpyqdph||66maF_7rM14o=PcxF3cN4XZqCg|B2=#mt%&axWClh??&=q~#@J z1I0KL(lMP-#Cf=XRC=wGM!3OgtX`Oj^OOxP+|8e&MwhiOT5w&5AVkCmmr&?FO1)wSwLbLW%NmEy%Ud0%qLXmrquWaPL~ zaF94NamBi5g~ARb?WD1Ec1C|tHQ=*VUF11mB@gjex%RYLC?7bWwp(dqO!}E>NQ1g- zW!X+wwVq0{WoM_p9HS1GzU_3^6SnK(;ML>^+_@!Zu$Cxfq0;Xn%-iN)JF|euH3T340SG_<0uX=z z1Rwwb2tWV=nZSrXrDlHx&`SDbG5sR||NfsFyQG+3o3=SI_8l!^f&c^{009U<00Izz z00bZa0SG*9fx|gNX-`z%4e3_@idA#`A2O2;%x zY2N@T(F2KWdKyqQXf-4!cvu|}ty*-E2On9zy?pmkmUh7o`0diK+n;${&j)x1;24yJpcdz literal 0 HcmV?d00001 diff --git a/solution/Makefile b/solution/Makefile index d72797d..ad13382 100644 --- a/solution/Makefile +++ b/solution/Makefile @@ -2,7 +2,7 @@ test: poetry run pytest coverage: - poetry run coverage + poetry run pytest --cov=src tests/ format: poetry run black . \ No newline at end of file diff --git a/solution/data/catalogue.yaml b/solution/data/catalogue.yaml index bcebf0b..944cbb5 100644 --- a/solution/data/catalogue.yaml +++ b/solution/data/catalogue.yaml @@ -17,4 +17,7 @@ vga: sku: vga name: VGA adapter price: 30.00 - currency: dollar \ No newline at end of file + currency: dollar +bad-item: + sku: bad-item + name: Bad Item For Testing \ No newline at end of file diff --git a/solution/poetry.lock b/solution/poetry.lock index 5dcda03..60df24d 100644 --- a/solution/poetry.lock +++ b/solution/poetry.lock @@ -82,6 +82,93 @@ files = [ {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] +[[package]] +name = "coverage" +version = "7.6.1" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, + {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, + {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, + {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, + {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, + {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, + {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, + {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, + {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, + {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, + {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, + {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, + {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, + {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, + {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, + {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, + {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, + {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, + {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, + {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, + {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, + {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, + {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, + {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, + {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, + {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, + {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, + {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, + {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, + {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, + {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, + {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, + {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, + {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, + {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, + {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, + {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, + {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, +] + +[package.dependencies] +tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} + +[package.extras] +toml = ["tomli"] + [[package]] name = "exceptiongroup" version = "1.2.2" @@ -317,6 +404,24 @@ tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-cov" +version = "5.0.0" +description = "Pytest plugin for measuring coverage." +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, + {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, +] + +[package.dependencies] +coverage = {version = ">=5.2.1", extras = ["toml"]} +pytest = ">=4.6" + +[package.extras] +testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] + [[package]] name = "pyyaml" version = "6.0.2" @@ -404,4 +509,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "a56d92b1f586be9dc534bde7db7efd33fa6c8fcb52d78c32d96295aef6191140" +content-hash = "35837864198a809febb02ebc3b21e71dac08bedb5ef4fb8d358d4bc73b0ac33c" diff --git a/solution/pyproject.toml b/solution/pyproject.toml index 49da10f..28e8445 100644 --- a/solution/pyproject.toml +++ b/solution/pyproject.toml @@ -12,6 +12,7 @@ pydantic = "^2.9.2" pyyaml = "^6.0.2" black = "^24.8.0" pytest = "^8.3.3" +pytest-cov = "^5.0.0" [build-system] diff --git a/solution/src/checkout.py b/solution/src/checkout.py index d53d4d9..301a3a6 100644 --- a/solution/src/checkout.py +++ b/solution/src/checkout.py @@ -1,4 +1,5 @@ from typing import List, Dict +from src.exceptions import ItemNotInCatalogue from src.models import PricingRule, Item import logging @@ -25,6 +26,8 @@ def scan(self, item_sku: str) -> None: item_sku (str): Unique identifier of the item. """ item_attrs = self.catalogue.get(item_sku) + if item_attrs is None: + raise ItemNotInCatalogue(f"Item with sku {item_sku} not found in catalogue.") item = Item(**item_attrs) self.scanned_items.append(item) diff --git a/solution/src/exceptions.py b/solution/src/exceptions.py new file mode 100644 index 0000000..f94f271 --- /dev/null +++ b/solution/src/exceptions.py @@ -0,0 +1,2 @@ +class ItemNotInCatalogue(Exception): + pass \ No newline at end of file diff --git a/solution/src/pricing_rules.py b/solution/src/pricing_rules.py index 2a95934..59d4af9 100644 --- a/solution/src/pricing_rules.py +++ b/solution/src/pricing_rules.py @@ -7,6 +7,15 @@ def count_matching_items(items: List[Item], sku_to_match: str): + """Counts the number of items in the list that match the provided sku + + Args: + items (List[Item]): Items to check + sku_to_match (str): Sku to check for + + Returns: + int: number of matching items + """ matching_items = [i for i in items if i.sku == sku_to_match] n_matching_items = len(matching_items) @@ -14,8 +23,8 @@ def count_matching_items(items: List[Item], sku_to_match: str): class NForMDeal: - """Logic to apply the N for M deal e.g. 3 for 2. - """ + """Logic to apply the N for M deal e.g. 3 for 2.""" + def __init__( self, *, @@ -57,8 +66,8 @@ def apply(self, items: List[Item]) -> List[Item]: class BulkDiscountDeal: - """Logic for the Bulk Discount Deal e.g. buy more than 4 of an item and have the price reduced for all of these items. - """ + """Logic for the Bulk Discount Deal e.g. buy more than 4 of an item and have the price reduced for all of these items.""" + def __init__( self, *, @@ -102,8 +111,8 @@ def apply(self, items: List[Item]) -> List[Item]: class FreeItemDeal: - """Logic for the Free Item Deal e.g. buy an item and receive a free item with it. - """ + """Logic for the Free Item Deal e.g. buy an item and receive a free item with it.""" + def __init__( self, *, diff --git a/solution/__init__.py b/solution/tests/__init__.py similarity index 100% rename from solution/__init__.py rename to solution/tests/__init__.py diff --git a/solution/tests/conftest.py b/solution/tests/conftest.py new file mode 100644 index 0000000..298c1f7 --- /dev/null +++ b/solution/tests/conftest.py @@ -0,0 +1,56 @@ +import pytest +import yaml +from src.pricing_rules import ( + FreeItemDeal, + BulkDiscountDeal, + NForMDeal +) +from src.models import Item + +@pytest.fixture +def mock_items(): + items = [ + Item( + sku="", + name="", + price=10.00, + currency="" + ), + Item( + sku="", + name="", + price=15.00, + currency="" + ) + ] + + return items + +@pytest.fixture +def pricing_rules(): + with open("data/specials.yaml") as f: + deals = yaml.safe_load(f) + + free_item_deals = deals["FreeItemDeals"] + bulk_discount_deals = deals["BulkDiscountDeals"] + n_for_m_deals = deals["NForMDeals"] + + pricing_rules = list() + + for deal in free_item_deals: + pricing_rules.append(FreeItemDeal(**deal)) + + for deal in bulk_discount_deals: + pricing_rules.append(BulkDiscountDeal(**deal)) + + for deal in n_for_m_deals: + pricing_rules.append(NForMDeal(**deal)) + + return pricing_rules + + +@pytest.fixture +def catalogue(): + with open("data/catalogue.yaml") as f: + catalogue = yaml.safe_load(f) + return catalogue \ No newline at end of file diff --git a/solution/tests/test.py b/solution/tests/test.py deleted file mode 100644 index e69de29..0000000 diff --git a/solution/tests/test_checkout.py b/solution/tests/test_checkout.py new file mode 100644 index 0000000..5db665e --- /dev/null +++ b/solution/tests/test_checkout.py @@ -0,0 +1,105 @@ +import pytest +from pydantic import ValidationError +from src.exceptions import ItemNotInCatalogue +from src.checkout import Checkout +from src.models import PricingRule, Item +from src.pricing_rules import count_matching_items +from typing import Dict, List + + +# test 1 - check scan adds item +def test_scan_adds_item(pricing_rules: PricingRule, catalogue: Dict[str, Item]): + co = Checkout( + pricing_rules=pricing_rules, + catalogue=catalogue + ) + + assert len(co.scanned_items) == 0 + + co.scan(item_sku="atv") + + assert len(co.scanned_items) == 1 + +# test 2 - check scan raises error if item doesn't exist +def test_scan_raises_item_not_in_catalogue(pricing_rules: PricingRule, catalogue: Dict[str, Item]): + co = Checkout( + pricing_rules=pricing_rules, + catalogue=catalogue + ) + + with pytest.raises(ItemNotInCatalogue, match=r".*not found in catalogue.*"): + co.scan("abc") + +# test 3 - check scan raises error if item has wrong attributes +def test_scan_raises_error_bad_attribute(pricing_rules: PricingRule, catalogue: Dict[str, Item]): + co = Checkout( + pricing_rules=pricing_rules, + catalogue=catalogue + ) + + with pytest.raises(ValidationError): + co.scan("bad-item") + +# test 4 - check total calculates price +def test_correct_price(): + co = Checkout( + pricing_rules=[], + catalogue={} + ) + + co.scanned_items = [ + Item( + sku="", + name="", + price=10.00, + currency="" + ), + Item( + sku="", + name="", + price=15.00, + currency="" + ) + ] + + assert co.total() == 25.00 + +# test 5 - check receipt returns items +def test_receipt(): + co = Checkout( + pricing_rules=[], + catalogue={} + ) + + co.items_with_deals = [ + Item( + sku="", + name="", + price=10.00, + currency="" + ), + Item( + sku="", + name="", + price=15.00, + currency="" + ) + ] + + assert co.receipt() == co.items_with_deals + +# test 6 - check count counts correctly +def test_count_matching_items(mock_items: List[Item]): + count = count_matching_items( + items=mock_items, + sku_to_match="" + ) + + assert count == 2 + + count = count_matching_items( + items=mock_items, + sku_to_match="abc" + ) + + assert count == 0 \ No newline at end of file From 2ebdd81f1c0f633dfa37049d115839ed8aa296a6 Mon Sep 17 00:00:00 2001 From: Richa Date: Mon, 23 Sep 2024 11:45:50 +0100 Subject: [PATCH 09/10] format --- solution/src/checkout.py | 4 +- solution/src/exceptions.py | 2 +- solution/tests/conftest.py | 26 +++-------- solution/tests/test_checkout.py | 82 ++++++++++----------------------- 4 files changed, 36 insertions(+), 78 deletions(-) diff --git a/solution/src/checkout.py b/solution/src/checkout.py index 301a3a6..0d453cd 100644 --- a/solution/src/checkout.py +++ b/solution/src/checkout.py @@ -27,7 +27,9 @@ def scan(self, item_sku: str) -> None: """ item_attrs = self.catalogue.get(item_sku) if item_attrs is None: - raise ItemNotInCatalogue(f"Item with sku {item_sku} not found in catalogue.") + raise ItemNotInCatalogue( + f"Item with sku {item_sku} not found in catalogue." + ) item = Item(**item_attrs) self.scanned_items.append(item) diff --git a/solution/src/exceptions.py b/solution/src/exceptions.py index f94f271..70868a9 100644 --- a/solution/src/exceptions.py +++ b/solution/src/exceptions.py @@ -1,2 +1,2 @@ class ItemNotInCatalogue(Exception): - pass \ No newline at end of file + pass diff --git a/solution/tests/conftest.py b/solution/tests/conftest.py index 298c1f7..2206f32 100644 --- a/solution/tests/conftest.py +++ b/solution/tests/conftest.py @@ -1,31 +1,19 @@ import pytest import yaml -from src.pricing_rules import ( - FreeItemDeal, - BulkDiscountDeal, - NForMDeal -) +from src.pricing_rules import FreeItemDeal, BulkDiscountDeal, NForMDeal from src.models import Item + @pytest.fixture def mock_items(): - items = [ - Item( - sku="", - name="", - price=10.00, - currency="" - ), - Item( - sku="", - name="", - price=15.00, - currency="" - ) + items = [ + Item(sku="", name="", price=10.00, currency=""), + Item(sku="", name="", price=15.00, currency=""), ] return items + @pytest.fixture def pricing_rules(): with open("data/specials.yaml") as f: @@ -53,4 +41,4 @@ def pricing_rules(): def catalogue(): with open("data/catalogue.yaml") as f: catalogue = yaml.safe_load(f) - return catalogue \ No newline at end of file + return catalogue diff --git a/solution/tests/test_checkout.py b/solution/tests/test_checkout.py index 5db665e..c75803d 100644 --- a/solution/tests/test_checkout.py +++ b/solution/tests/test_checkout.py @@ -9,10 +9,7 @@ # test 1 - check scan adds item def test_scan_adds_item(pricing_rules: PricingRule, catalogue: Dict[str, Item]): - co = Checkout( - pricing_rules=pricing_rules, - catalogue=catalogue - ) + co = Checkout(pricing_rules=pricing_rules, catalogue=catalogue) assert len(co.scanned_items) == 0 @@ -20,86 +17,57 @@ def test_scan_adds_item(pricing_rules: PricingRule, catalogue: Dict[str, Item]): assert len(co.scanned_items) == 1 + # test 2 - check scan raises error if item doesn't exist -def test_scan_raises_item_not_in_catalogue(pricing_rules: PricingRule, catalogue: Dict[str, Item]): - co = Checkout( - pricing_rules=pricing_rules, - catalogue=catalogue - ) +def test_scan_raises_item_not_in_catalogue( + pricing_rules: PricingRule, catalogue: Dict[str, Item] +): + co = Checkout(pricing_rules=pricing_rules, catalogue=catalogue) with pytest.raises(ItemNotInCatalogue, match=r".*not found in catalogue.*"): co.scan("abc") + # test 3 - check scan raises error if item has wrong attributes -def test_scan_raises_error_bad_attribute(pricing_rules: PricingRule, catalogue: Dict[str, Item]): - co = Checkout( - pricing_rules=pricing_rules, - catalogue=catalogue - ) +def test_scan_raises_error_bad_attribute( + pricing_rules: PricingRule, catalogue: Dict[str, Item] +): + co = Checkout(pricing_rules=pricing_rules, catalogue=catalogue) with pytest.raises(ValidationError): co.scan("bad-item") -# test 4 - check total calculates price + +# test 4 - check total calculates price def test_correct_price(): - co = Checkout( - pricing_rules=[], - catalogue={} - ) + co = Checkout(pricing_rules=[], catalogue={}) co.scanned_items = [ - Item( - sku="", - name="", - price=10.00, - currency="" - ), - Item( - sku="", - name="", - price=15.00, - currency="" - ) + Item(sku="", name="", price=10.00, currency=""), + Item(sku="", name="", price=15.00, currency=""), ] assert co.total() == 25.00 + # test 5 - check receipt returns items def test_receipt(): - co = Checkout( - pricing_rules=[], - catalogue={} - ) + co = Checkout(pricing_rules=[], catalogue={}) co.items_with_deals = [ - Item( - sku="", - name="", - price=10.00, - currency="" - ), - Item( - sku="", - name="", - price=15.00, - currency="" - ) + Item(sku="", name="", price=10.00, currency=""), + Item(sku="", name="", price=15.00, currency=""), ] assert co.receipt() == co.items_with_deals - + + # test 6 - check count counts correctly def test_count_matching_items(mock_items: List[Item]): - count = count_matching_items( - items=mock_items, - sku_to_match="" - ) + count = count_matching_items(items=mock_items, sku_to_match="") assert count == 2 - count = count_matching_items( - items=mock_items, - sku_to_match="abc" - ) + count = count_matching_items(items=mock_items, sku_to_match="abc") - assert count == 0 \ No newline at end of file + assert count == 0 From e97559d6e84f769a2e877df8c44026d51689fc5c Mon Sep 17 00:00:00 2001 From: Richa Date: Mon, 23 Sep 2024 11:59:51 +0100 Subject: [PATCH 10/10] better cov --- solution/.coverage | Bin 53248 -> 53248 bytes solution/README.md | 2 +- solution/src/pricing_rules.py | 79 +++++++++++++++++++++----------- solution/tests/conftest.py | 1 + solution/tests/test_checkout.py | 15 +++++- 5 files changed, 69 insertions(+), 28 deletions(-) diff --git a/solution/.coverage b/solution/.coverage index e755abe3191a835ac83b84667f05b9c6c9ff6bbe..9857a88048e65b32dfb3c26667040bfe6023269d 100644 GIT binary patch delta 85 zcmZozz}&Eac>`Mm&prnJpZpK_ukfGY-?v#%U=9D|{r%hqVrvxGSXdZ2`B>Pvn1Qqy o^E2**rA*>XK(+x>7*oW1-T$l%3<*E=85kH&|7YF&v!C4o08T>~v;Y7A delta 84 zcmZozz}&Eac>`Mm&wd8}pZpK`ukxSe-@jQ List[Item]: + """Updates items in list with matching sku + + Args: + items (List[Item]): Items to search through + sku (str): Sku to match + n (int): maximum number of matching items to update + attr_to_update (str): attribute to update + new_value (Any): new value of attribute + + Returns: + List[Item]: _description_ + """ + for ix, item in enumerate(items): + if item.sku == sku: + items[ix] = items[ix].update( + attr_to_update=attr_to_update, new_value=new_value + ) + + n -= 1 + if n <= 0: + break + return items + + class NForMDeal: """Logic to apply the N for M deal e.g. 3 for 2.""" @@ -54,13 +81,13 @@ def apply(self, items: List[Item]) -> List[Item]: logger.info("Qualifies for NForMDeal") n_free_items = self.purchase_number_required // self.number_to_pay_for - for ix, item in enumerate(items): - if item.sku == self.sku: - items[ix] = items[ix].update(attr_to_update="price", new_value=0.0) - - n_free_items -= 1 - if n_free_items <= 0: - break + items = update_items( + items=items, + sku=self.sku, + n=n_free_items, + attr_to_update="price", + new_value=0, + ) return items @@ -97,15 +124,13 @@ def apply(self, items: List[Item]) -> List[Item]: logger.info("Qualifies for Bulk Discount Deal") n_repriced_items = self.n_matching_items - for ix, item in enumerate(items): - if item.sku == self.sku: - items[ix] = items[ix].update( - attr_to_update="price", new_value=self.new_item_price - ) - - n_repriced_items -= 1 - if n_repriced_items <= 0: - break + items = update_items( + items=items, + sku=self.sku, + n=n_repriced_items, + attr_to_update="price", + new_value=self.new_item_price, + ) return items @@ -143,24 +168,26 @@ def apply(self, items: List[Item]) -> List[Item]: logger.info("Qualifies for Free Item Deal") # if item sku is already in items, make it free # the deal says we get n_free_items for every purchase_number_required - number_for_free = ( + n_free_items = ( self.n_matching_items // self.purchase_number_required ) * self.n_free_items + # if there are any of the free items already in the list of items, make these free first - for ix, item in enumerate(items): - if item.sku == self.item_sku: - items[ix] = items[ix].update(attr_to_update="price", new_value=0.0) - number_for_free -= 1 - if number_for_free <= 0: - break + items = update_items( + items=items, + sku=self.item_sku, + n=n_free_items, + attr_to_update="price", + new_value=0.0, + ) # if number_for_free still more than 0, add a free one - while number_for_free > 0: + while n_free_items > 0: items.append( Item( sku=self.item_sku, name="FreeItem", price=0.0, currency="dollar" ) ) - number_for_free -= 1 + n_free_items -= 1 return items diff --git a/solution/tests/conftest.py b/solution/tests/conftest.py index 2206f32..9f021cd 100644 --- a/solution/tests/conftest.py +++ b/solution/tests/conftest.py @@ -9,6 +9,7 @@ def mock_items(): items = [ Item(sku="", name="", price=10.00, currency=""), Item(sku="", name="", price=15.00, currency=""), + Item(sku="to-update", name="", price=0.00, currency=""), ] return items diff --git a/solution/tests/test_checkout.py b/solution/tests/test_checkout.py index c75803d..8dd3a4e 100644 --- a/solution/tests/test_checkout.py +++ b/solution/tests/test_checkout.py @@ -3,7 +3,7 @@ from src.exceptions import ItemNotInCatalogue from src.checkout import Checkout from src.models import PricingRule, Item -from src.pricing_rules import count_matching_items +from src.pricing_rules import count_matching_items, update_items from typing import Dict, List @@ -71,3 +71,16 @@ def test_count_matching_items(mock_items: List[Item]): count = count_matching_items(items=mock_items, sku_to_match="abc") assert count == 0 + + +# test 7 - check update items updates correct items +def test_check_update_items(mock_items: List[Item]): + updated_items = update_items( + items=mock_items, sku="to-update", attr_to_update="price", new_value=10.00, n=1 + ) + + assert updated_items == [ + Item(sku="", name="", price=10.00, currency=""), + Item(sku="", name="", price=15.00, currency=""), + Item(sku="to-update", name="", price=10.00, currency=""), + ]