diff --git a/Makefile b/Makefile index 5d0b70c..82bf93e 100644 --- a/Makefile +++ b/Makefile @@ -1,6 +1,10 @@ .DEFAULT_GOAL := help SHELL := /bin/bash +.PHONY: api +api: ## Run API + @poetry run uvicorn poker.api:app + .PHONY: black black: ## Run black formatter @poetry run black poker tests; diff --git a/README.md b/README.md index 4b8d985..d6cc434 100644 --- a/README.md +++ b/README.md @@ -88,3 +88,18 @@ Result: "full house: 4 over 2" Query: "6H 7H 8H 9H 10H" Result: "straight flush: 10-high diamonds" ``` + +## Requirements +- Python 3.11 +- [Poetry](https://python-poetry.org/) +- [GNU Make](https://www.gnu.org/software/make/) + +## Usage +- Install with `make install`. +- Run linting and tests with `make quality test coverage clean`. +- Run API with `make API`. +- Check API health with `curl localhost:8000` +- Query API for rank with e.g. + ```shell + curl -X POST -d '2H 3D 5S 10C KD' localhost:8000/rank + ``` diff --git a/poetry.lock b/poetry.lock index 3e3ef63..f64edbe 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,3 +1,20 @@ +[[package]] +name = "anyio" +version = "3.6.2" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +category = "main" +optional = false +python-versions = ">=3.6.2" + +[package.dependencies] +idna = ">=2.8" +sniffio = ">=1.1" + +[package.extras] +doc = ["packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["contextlib2", "coverage[toml] (>=4.5)", "hypothesis (>=4.0)", "mock (>=4)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (<0.15)", "uvloop (>=0.15)"] +trio = ["trio (>=0.16,<0.22)"] + [[package]] name = "attrs" version = "22.1.0" @@ -32,11 +49,19 @@ d = ["aiohttp (>=3.7.4)"] jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"] uvloop = ["uvloop (>=0.15.2)"] +[[package]] +name = "certifi" +version = "2022.9.24" +description = "Python package for providing Mozilla's CA Bundle." +category = "dev" +optional = false +python-versions = ">=3.6" + [[package]] name = "click" version = "8.1.3" description = "Composable command line interface toolkit" -category = "dev" +category = "main" optional = false python-versions = ">=3.7" @@ -47,7 +72,7 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""} name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" +category = "main" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" @@ -70,6 +95,24 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "fastapi" +version = "0.87.0" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +pydantic = ">=1.6.2,<1.7 || >1.7,<1.7.1 || >1.7.1,<1.7.2 || >1.7.2,<1.7.3 || >1.7.3,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0" +starlette = "0.21.0" + +[package.extras] +all = ["email-validator (>=1.1.1)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "python-multipart (>=0.0.5)", "pyyaml (>=5.3.1)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0)", "uvicorn[standard] (>=0.12.0)"] +dev = ["pre-commit (>=2.17.0,<3.0.0)", "ruff (==0.0.114)", "uvicorn[standard] (>=0.12.0,<0.19.0)"] +doc = ["mdx-include (>=1.4.1,<2.0.0)", "mkdocs (>=1.1.2,<2.0.0)", "mkdocs-markdownextradata-plugin (>=0.1.7,<0.3.0)", "mkdocs-material (>=8.1.4,<9.0.0)", "pyyaml (>=5.3.1,<7.0.0)", "typer[all] (>=0.6.1,<0.7.0)"] +test = ["anyio[trio] (>=3.2.1,<4.0.0)", "black (==22.8.0)", "coverage[toml] (>=6.5.0,<7.0)", "databases[sqlite] (>=0.3.2,<0.7.0)", "email-validator (>=1.1.1,<2.0.0)", "flask (>=1.1.2,<3.0.0)", "httpx (>=0.23.0,<0.24.0)", "isort (>=5.0.6,<6.0.0)", "mypy (==0.982)", "orjson (>=3.2.1,<4.0.0)", "passlib[bcrypt] (>=1.7.2,<2.0.0)", "peewee (>=3.13.3,<4.0.0)", "pytest (>=7.1.3,<8.0.0)", "python-jose[cryptography] (>=3.3.0,<4.0.0)", "python-multipart (>=0.0.5,<0.0.6)", "pyyaml (>=5.3.1,<7.0.0)", "ruff (==0.0.114)", "sqlalchemy (>=1.3.18,<=1.4.41)", "types-orjson (==3.6.2)", "types-ujson (==5.5.0)", "ujson (>=4.0.1,!=4.0.2,!=4.1.0,!=4.2.0,!=4.3.0,!=5.0.0,!=5.1.0,<6.0.0)"] + [[package]] name = "flake8" version = "5.0.4" @@ -134,6 +177,60 @@ ci = ["coverage (>=4.0.0,<5.0.0)", "coveralls", "flake8-builtins", "flake8-comma dev = ["coverage (>=4.0.0,<5.0.0)", "flake8-builtins", "flake8-commas", "flake8-fixme", "flake8-print", "flake8-quotes", "flake8-todo", "pytest (>=4)", "pytest-cov (>=2)"] test = ["coverage (>=4.0.0,<5.0.0)", "flake8-builtins", "flake8-commas", "flake8-fixme", "flake8-print", "flake8-quotes", "flake8-todo", "pytest (>=4)", "pytest-cov (>=2)"] +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +category = "main" +optional = false +python-versions = ">=3.7" + +[[package]] +name = "httpcore" +version = "0.16.1" +description = "A minimal low-level HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.0,<5.0" +certifi = "*" +h11 = ">=0.13,<0.15" +sniffio = ">=1.0.0,<2.0.0" + +[package.extras] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "httpx" +version = "0.23.1" +description = "The next generation HTTP client." +category = "dev" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +certifi = "*" +httpcore = ">=0.15.0,<0.17.0" +rfc3986 = {version = ">=1.3,<2", extras = ["idna2008"]} +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (>=8.0.0,<9.0.0)", "pygments (>=2.0.0,<3.0.0)", "rich (>=10,<13)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (>=1.0.0,<2.0.0)"] + +[[package]] +name = "idna" +version = "3.4" +description = "Internationalized Domain Names in Applications (IDNA)" +category = "main" +optional = false +python-versions = ">=3.5" + [[package]] name = "iniconfig" version = "1.1.1" @@ -156,6 +253,21 @@ pipfile_deprecated_finder = ["pipreqs", "requirementslib"] plugins = ["setuptools"] requirements_deprecated_finder = ["pip-api", "pipreqs"] +[[package]] +name = "loguru" +version = "0.6.0" +description = "Python logging made (stupidly) simple" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.dependencies] +colorama = {version = ">=0.3.4", markers = "sys_platform == \"win32\""} +win32-setctime = {version = ">=1.0.0", markers = "sys_platform == \"win32\""} + +[package.extras] +dev = ["Sphinx (>=4.1.1)", "black (>=19.10b0)", "colorama (>=0.3.4)", "docutils (==0.16)", "flake8 (>=3.7.7)", "isort (>=5.1.1)", "pytest (>=4.6.2)", "pytest-cov (>=2.7.1)", "sphinx-autobuild (>=0.7.1)", "sphinx-rtd-theme (>=0.4.3)", "tox (>=3.9.0)"] + [[package]] name = "mccabe" version = "0.7.0" @@ -307,6 +419,28 @@ pluggy = ">=0.12,<2.0" [package.extras] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"] +[[package]] +name = "rfc3986" +version = "1.5.0" +description = "Validating URI References per RFC 3986" +category = "dev" +optional = false +python-versions = "*" + +[package.dependencies] +idna = {version = "*", optional = true, markers = "extra == \"idna2008\""} + +[package.extras] +idna2008 = ["idna"] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +category = "main" +optional = false +python-versions = ">=3.7" + [[package]] name = "snowballstemmer" version = "2.2.0" @@ -315,6 +449,20 @@ category = "dev" optional = false python-versions = "*" +[[package]] +name = "starlette" +version = "0.21.0" +description = "The little ASGI library that shines." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart", "pyyaml"] + [[package]] name = "typing-extensions" version = "4.4.0" @@ -323,12 +471,42 @@ category = "main" optional = false python-versions = ">=3.7" +[[package]] +name = "uvicorn" +version = "0.20.0" +description = "The lightning-fast ASGI server." +category = "main" +optional = false +python-versions = ">=3.7" + +[package.dependencies] +click = ">=7.0" +h11 = ">=0.8" + +[package.extras] +standard = ["colorama (>=0.4)", "httptools (>=0.5.0)", "python-dotenv (>=0.13)", "pyyaml (>=5.1)", "uvloop (>=0.14.0,!=0.15.0,!=0.15.1)", "watchfiles (>=0.13)", "websockets (>=10.4)"] + +[[package]] +name = "win32-setctime" +version = "1.1.0" +description = "A small Python utility to set file creation time on Windows" +category = "main" +optional = false +python-versions = ">=3.5" + +[package.extras] +dev = ["black (>=19.3b0)", "pytest (>=4.6.2)"] + [metadata] lock-version = "1.1" python-versions = ">=3.11,<3.12" -content-hash = "78fbc979be8f154829b70aebcbd4016a447d6415206c52aaf7c2505c3160b23f" +content-hash = "13bef4122cae8394c862b2497c696d777b88604803563af278fbbfdc0d7c3b78" [metadata.files] +anyio = [ + {file = "anyio-3.6.2-py3-none-any.whl", hash = "sha256:fbbe32bd270d2a2ef3ed1c5d45041250284e31fc0a4df4a5a6071842051a51e3"}, + {file = "anyio-3.6.2.tar.gz", hash = "sha256:25ea0d673ae30af41a0c442f81cf3b38c7e79fdc7b60335a4c14e05eb0947421"}, +] attrs = [ {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, @@ -356,6 +534,10 @@ black = [ {file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"}, {file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"}, ] +certifi = [ + {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, + {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, +] click = [ {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, @@ -420,6 +602,10 @@ eradicate = [ {file = "eradicate-2.1.0-py3-none-any.whl", hash = "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2"}, {file = "eradicate-2.1.0.tar.gz", hash = "sha256:aac7384ab25b1bf21c4c012de9b4bf8398945a14c98c911545b2ea50ab558014"}, ] +fastapi = [ + {file = "fastapi-0.87.0-py3-none-any.whl", hash = "sha256:254453a2e22f64e2a1b4e1d8baf67d239e55b6c8165c079d25746a5220c81bb4"}, + {file = "fastapi-0.87.0.tar.gz", hash = "sha256:07032e53df9a57165047b4f38731c38bdcc3be5493220471015e2b4b51b486a4"}, +] flake8 = [ {file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"}, {file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"}, @@ -438,6 +624,22 @@ flake8-eradicate = [ flake8-use-fstring = [ {file = "flake8-use-fstring-1.4.tar.gz", hash = "sha256:6550bf722585eb97dffa8343b0f1c372101f5c4ab5b07ebf0edd1c79880cdd39"}, ] +h11 = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] +httpcore = [ + {file = "httpcore-0.16.1-py3-none-any.whl", hash = "sha256:8d393db683cc8e35cc6ecb02577c5e1abfedde52b38316d038932a84b4875ecb"}, + {file = "httpcore-0.16.1.tar.gz", hash = "sha256:3d3143ff5e1656a5740ea2f0c167e8e9d48c5a9bbd7f00ad1f8cff5711b08543"}, +] +httpx = [ + {file = "httpx-0.23.1-py3-none-any.whl", hash = "sha256:0b9b1f0ee18b9978d637b0776bfd7f54e2ca278e063e3586d8f01cda89e042a8"}, + {file = "httpx-0.23.1.tar.gz", hash = "sha256:202ae15319be24efe9a8bd4ed4360e68fde7b38bcc2ce87088d416f026667d19"}, +] +idna = [ + {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, + {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, +] iniconfig = [ {file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"}, {file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"}, @@ -446,6 +648,10 @@ isort = [ {file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"}, {file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"}, ] +loguru = [ + {file = "loguru-0.6.0-py3-none-any.whl", hash = "sha256:4e2414d534a2ab57573365b3e6d0234dfb1d84b68b7f3b948e6fb743860a77c3"}, + {file = "loguru-0.6.0.tar.gz", hash = "sha256:066bd06758d0a513e9836fd9c6b5a75bfb3fd36841f4b996bc60b547a309d41c"}, +] mccabe = [ {file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"}, {file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"}, @@ -560,11 +766,31 @@ pytest = [ {file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"}, {file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"}, ] +rfc3986 = [ + {file = "rfc3986-1.5.0-py2.py3-none-any.whl", hash = "sha256:a86d6e1f5b1dc238b218b012df0aa79409667bb209e58da56d0b94704e712a97"}, + {file = "rfc3986-1.5.0.tar.gz", hash = "sha256:270aaf10d87d0d4e095063c65bf3ddbc6ee3d0b226328ce21e036f946e421835"}, +] +sniffio = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] snowballstemmer = [ {file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"}, {file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"}, ] +starlette = [ + {file = "starlette-0.21.0-py3-none-any.whl", hash = "sha256:0efc058261bbcddeca93cad577efd36d0c8a317e44376bcfc0e097a2b3dc24a7"}, + {file = "starlette-0.21.0.tar.gz", hash = "sha256:b1b52305ee8f7cfc48cde383496f7c11ab897cd7112b33d998b1317dc8ef9027"}, +] typing-extensions = [ {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, ] +uvicorn = [ + {file = "uvicorn-0.20.0-py3-none-any.whl", hash = "sha256:c3ed1598a5668208723f2bb49336f4509424ad198d6ab2615b7783db58d919fd"}, + {file = "uvicorn-0.20.0.tar.gz", hash = "sha256:a4e12017b940247f836bc90b72e725d7dfd0c8ed1c51eb365f5ba30d9f5127d8"}, +] +win32-setctime = [ + {file = "win32_setctime-1.1.0-py3-none-any.whl", hash = "sha256:231db239e959c2fe7eb1d7dc129f11172354f98361c4fa2d6d2d7e278baa8aad"}, + {file = "win32_setctime-1.1.0.tar.gz", hash = "sha256:15cf5750465118d6929ae4de4eb46e8edae9a5634350c01ba582df868e932cb2"}, +] diff --git a/poker/api.py b/poker/api.py new file mode 100644 index 0000000..9faa89b --- /dev/null +++ b/poker/api.py @@ -0,0 +1,56 @@ +from fastapi import Body, FastAPI +from loguru import logger + +from poker.constants import Suit, Value +from poker.models import Card, Hand +from poker.rank import rank_hand + +app = FastAPI() + +_CARD_PATTERN = r"\s".join(5 * ["(2|3|4|5|6|7|8|9|10|J|K|Q|A)[CDHS]"]) +_SUIT_MAP = { + "C": Suit.CLUBS, + "D": Suit.DIAMONDS, + "H": Suit.HEARTS, + "S": Suit.SPADES, +} +_VALUE_MAP = { + "2": Value.TWO, + "3": Value.THREE, + "4": Value.FOUR, + "5": Value.FIVE, + "6": Value.SIX, + "7": Value.SEVEN, + "8": Value.EIGHT, + "9": Value.NINE, + "10": Value.TEN, + "J": Value.JACK, + "Q": Value.QUEEN, + "K": Value.KING, + "A": Value.ACE, +} + + +@app.get("/") +async def health_check() -> str: + """Health check endpoint.""" + return "OK" + + +@app.post("/rank") +def rank(body: str = Body(regex=_CARD_PATTERN, example="2H 3D 5S 10C KD")) -> str: + """Rank hand. + + TODO: Improve error response. + """ + logger.info(f"Input hand: {body}") + cards = [ + Card(suit=_SUIT_MAP[card[-1]], value=_VALUE_MAP[card[:-1]]) + for card in body.split() + ] + + ranked_hand = rank_hand(Hand(cards=cards)) + + logger.info(f"Hand rank: {ranked_hand.rank}") + + return ranked_hand.description diff --git a/poker/constants.py b/poker/constants.py index e0a7348..6402ef7 100644 --- a/poker/constants.py +++ b/poker/constants.py @@ -17,6 +17,11 @@ class Rank(IntEnum): PAIR = 9 HIGH_CARD = 10 + def __str__(self) -> str: + """Return string representation.""" + out: str = self.name.lower().replace("_", " ").title() + return out + class Suit(AutoName): """Card suit enum.""" @@ -46,7 +51,7 @@ class Value(IntEnum): def __str__(self) -> str: """Return string representation.""" - if self.value in [1, 11, 12, 13]: + if self.value in [1, 11, 12, 13, 14]: out: str = self.name.lower() else: out = str(self.value) diff --git a/poker/rank/hands.py b/poker/rank/hands.py index 6a81af5..a622f87 100644 --- a/poker/rank/hands.py +++ b/poker/rank/hands.py @@ -162,7 +162,7 @@ def rank_hand(hand: Hand) -> RankedHand: for rank, func in _FUNCTIONS.items(): result = func(hand) if result: - out = RankedHand( + return RankedHand( cards=hand.cards, rank=rank, description=DESCRIPTIONS[rank].format(**result), @@ -170,5 +170,3 @@ def rank_hand(hand: Hand) -> RankedHand: if out is None: raise ValueError("No rank found.") - - return out diff --git a/pyproject.toml b/pyproject.toml index 036f515..1df9ae7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,21 @@ [tool.poetry] name = "poker" -version = "1.1.0" +version = "2.0.0" description = "Single poker hand ranking service." authors = ["Paul Harrison"] readme = "README.md" [tool.poetry.dependencies] python = ">=3.11,<3.12" +fastapi = "^0.87.0" +loguru = "^0.6.0" pydantic = "^1.10.2" +uvicorn = "^0.20.0" [tool.poetry.group.test.dependencies] coverage = "^6.5.0" pytest = "^7.2.0" +httpx = "^0.23.1" [tool.poetry.group.lint.dependencies] diff --git a/tests/test_api.py b/tests/test_api.py new file mode 100644 index 0000000..8d9768f --- /dev/null +++ b/tests/test_api.py @@ -0,0 +1,67 @@ +import pytest +from fastapi.testclient import TestClient + +from poker.api import app + + +@pytest.fixture(scope="function") +def client() -> TestClient: + return TestClient(app) + + +def test_health_check(client: TestClient) -> None: + response = client.get("/") + + assert response.status_code == 200 + assert response.json() == "OK" + + +@pytest.mark.parametrize( + "body,expected", + [ + ("AH KH QH JH 10H", "royal flush: hearts"), + ("6H 7H 8H 9H 10H", "straight flush: 10-high hearts"), + ("AH AC AD AS KH", "four of a kind: ace"), + ("AH AC AD KS KH", "full house: ace over king"), + ("KC 10C 8C 7C 5C", "flush: clubs"), + ("10H 9C 8D 7S 6H", "straight: 10-high"), + ("AH AC AD KS QH", "three of a kind: ace"), + ("AH AC KD KS 7H", "two pair: ace and king"), + ("AH AC KD JS 7H", "pair: ace"), + ("AH KC QD 9S 7H", "high card: ace"), + ], + ids=[ + "royal-flush", + "straight-flush", + "four-of-a-kind", + "full-house", + "flush", + "straight", + "three-of-a-kind", + "two-pair", + "pair", + "high-card", + ], +) +def test_rank(client: TestClient, body: str, expected: str) -> None: + response = client.post("/rank", json=body) + + assert response.status_code == 200 + assert response.json() == expected + + +@pytest.mark.parametrize( + "body", + [ + "AH KH QH JH", + "AH KH QH JH 99W", + ], + ids=[ + "too-few", + "not-a-card", + ], +) +def test_rank_bad_input(client: TestClient, body: str) -> None: + response = client.post("/rank", json=body) + + assert response.status_code != 200