Compare commits
10 Commits
f0af94aa41
...
c114366357
Author | SHA1 | Date |
---|---|---|
Paul Harrison | c114366357 | |
Paul Harrison | 484c636513 | |
Paul Harrison | 9baafc80ed | |
Paul Harrison | b4ba284dcc | |
Paul Harrison | d924e39608 | |
Paul Harrison | 5393e7b799 | |
Paul Harrison | 68bfee17f1 | |
Paul Harrison | 6be02c447d | |
Paul Harrison | c337984965 | |
Paul Harrison | 2ba3a42b9b |
|
@ -0,0 +1,31 @@
|
||||||
|
FROM python:3.11-slim AS os-cache
|
||||||
|
|
||||||
|
ENV TZ=UTC
|
||||||
|
ENV PYTHONUNBUFFERED=1
|
||||||
|
ENV POETRY_NO_INTERACTION=1
|
||||||
|
ENV POETRY_VIRTUALENVS_CREATE=true
|
||||||
|
ENV POETRY_VIRTUALENVS_IN_PROJECT=true
|
||||||
|
|
||||||
|
ARG DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN apt update -y
|
||||||
|
RUN apt upgrade -y
|
||||||
|
RUN apt install -y build-essential
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# FIXME: README.md seemed to be required here but I'm not sure why!
|
||||||
|
COPY poetry.lock pyproject.toml Makefile README.md ./
|
||||||
|
|
||||||
|
RUN pip install "poetry~=1.2"
|
||||||
|
|
||||||
|
FROM os-cache AS image
|
||||||
|
|
||||||
|
COPY poker ./poker
|
||||||
|
RUN make install-prod
|
||||||
|
|
||||||
|
FROM os-cache AS test-image
|
||||||
|
|
||||||
|
COPY setup.cfg ./
|
||||||
|
COPY poker ./poker
|
||||||
|
COPY tests ./tests
|
||||||
|
RUN make install
|
16
Makefile
16
Makefile
|
@ -1,6 +1,10 @@
|
||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
SHELL := /bin/bash
|
SHELL := /bin/bash
|
||||||
|
|
||||||
|
.PHONY: api
|
||||||
|
api: ## Run API
|
||||||
|
@poetry run uvicorn --host 0.0.0.0 poker.api:app
|
||||||
|
|
||||||
.PHONY: black
|
.PHONY: black
|
||||||
black: ## Run black formatter
|
black: ## Run black formatter
|
||||||
@poetry run black poker tests;
|
@poetry run black poker tests;
|
||||||
|
@ -9,6 +13,10 @@ black: ## Run black formatter
|
||||||
black-check: ## Run black formatter
|
black-check: ## Run black formatter
|
||||||
@poetry run black poker tests --check;
|
@poetry run black poker tests --check;
|
||||||
|
|
||||||
|
.PHONY: build
|
||||||
|
build: ## Build Docker container
|
||||||
|
@COMPOSE_DOCKER_CLI_BUILD=1 DOCKER_BUILDKIT=1 docker compose -f compose.yml build;
|
||||||
|
|
||||||
.PHONY: clean
|
.PHONY: clean
|
||||||
clean: ## Remove python cache files
|
clean: ## Remove python cache files
|
||||||
-@find . -name '*.pyc' -exec rm -f {} +;
|
-@find . -name '*.pyc' -exec rm -f {} +;
|
||||||
|
@ -18,6 +26,10 @@ clean: ## Remove python cache files
|
||||||
-@find . -name '*.pytest_cache' -exec rm -fr {} +;
|
-@find . -name '*.pytest_cache' -exec rm -fr {} +;
|
||||||
-@find . -name '*.coverage' -exec rm -fr {} +;
|
-@find . -name '*.coverage' -exec rm -fr {} +;
|
||||||
|
|
||||||
|
.PHONY: coverage
|
||||||
|
coverage: ## Report test coverage
|
||||||
|
@poetry run coverage report --rcfile=setup.cfg;
|
||||||
|
|
||||||
.PHONY: flake8
|
.PHONY: flake8
|
||||||
flake8: ## Run flake8 linting
|
flake8: ## Run flake8 linting
|
||||||
@poetry run flake8 poker tests --config=setup.cfg;
|
@poetry run flake8 poker tests --config=setup.cfg;
|
||||||
|
@ -63,7 +75,7 @@ quality: flake8 mypy isort-check black-check pydocstyle ## Run linting checks
|
||||||
|
|
||||||
.PHONY: test
|
.PHONY: test
|
||||||
test: ## Run test pipeline
|
test: ## Run test pipeline
|
||||||
@poetry run pytest -c=setup.cfg -x
|
@poetry run coverage run --rcfile=setup.cfg --source=poker -m pytest -c=setup.cfg -x
|
||||||
|
|
||||||
.PHONY: uninstall
|
.PHONY: uninstall
|
||||||
uninstall: ## Remove virtual environment
|
uninstall: ## Remove virtual environment
|
||||||
|
@ -71,4 +83,4 @@ uninstall: ## Remove virtual environment
|
||||||
|
|
||||||
.PHONY: update
|
.PHONY: update
|
||||||
update: ## Update poetry.lock using pyproject.toml
|
update: ## Update poetry.lock using pyproject.toml
|
||||||
@poetry update;
|
@poetry update;
|
||||||
|
|
19
README.md
19
README.md
|
@ -1,6 +1,6 @@
|
||||||
# Single Poker Hand Ranking Service
|
# Single Poker Hand Ranking Service
|
||||||
## Requirements
|
## Project Scope
|
||||||
This service comprises an API to compute the rank of an individual poker hand. The requirements are to:
|
This service comprises an API to compute the rank of an individual poker hand. The scope of this project is to:
|
||||||
|
|
||||||
- Write an algorithm that takes a hand of cards and identifies the ranking of the given hand.
|
- Write an algorithm that takes a hand of cards and identifies the ranking of the given hand.
|
||||||
- Expose an API to serve this algorithm via an endpoint `/rank`, that accepts a valid poker hand and returns its ranking.
|
- Expose an API to serve this algorithm via an endpoint `/rank`, that accepts a valid poker hand and returns its ranking.
|
||||||
|
@ -88,3 +88,18 @@ Result: "full house: 4 over 2"
|
||||||
Query: "6H 7H 8H 9H 10H"
|
Query: "6H 7H 8H 9H 10H"
|
||||||
Result: "straight flush: 10-high diamonds"
|
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`.
|
||||||
|
- Build the API container with `make build`, then run API with `docker compose up -d`.
|
||||||
|
- 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
|
||||||
|
```
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
services:
|
||||||
|
api:
|
||||||
|
container_name: poker-api
|
||||||
|
build:
|
||||||
|
context: .
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
ports:
|
||||||
|
- "8000:8000"
|
||||||
|
command: make api
|
|
@ -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]]
|
[[package]]
|
||||||
name = "attrs"
|
name = "attrs"
|
||||||
version = "22.1.0"
|
version = "22.1.0"
|
||||||
|
@ -32,11 +49,19 @@ d = ["aiohttp (>=3.7.4)"]
|
||||||
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
jupyter = ["ipython (>=7.8.0)", "tokenize-rt (>=3.2.0)"]
|
||||||
uvloop = ["uvloop (>=0.15.2)"]
|
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]]
|
[[package]]
|
||||||
name = "click"
|
name = "click"
|
||||||
version = "8.1.3"
|
version = "8.1.3"
|
||||||
description = "Composable command line interface toolkit"
|
description = "Composable command line interface toolkit"
|
||||||
category = "dev"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
@ -47,10 +72,21 @@ colorama = {version = "*", markers = "platform_system == \"Windows\""}
|
||||||
name = "colorama"
|
name = "colorama"
|
||||||
version = "0.4.6"
|
version = "0.4.6"
|
||||||
description = "Cross-platform colored terminal text."
|
description = "Cross-platform colored terminal text."
|
||||||
category = "dev"
|
category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "coverage"
|
||||||
|
version = "6.5.0"
|
||||||
|
description = "Code coverage measurement for Python"
|
||||||
|
category = "dev"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
toml = ["tomli"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "eradicate"
|
name = "eradicate"
|
||||||
version = "2.1.0"
|
version = "2.1.0"
|
||||||
|
@ -59,6 +95,24 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
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]]
|
[[package]]
|
||||||
name = "flake8"
|
name = "flake8"
|
||||||
version = "5.0.4"
|
version = "5.0.4"
|
||||||
|
@ -123,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)"]
|
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)"]
|
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]]
|
[[package]]
|
||||||
name = "iniconfig"
|
name = "iniconfig"
|
||||||
version = "1.1.1"
|
version = "1.1.1"
|
||||||
|
@ -145,6 +253,21 @@ pipfile_deprecated_finder = ["pipreqs", "requirementslib"]
|
||||||
plugins = ["setuptools"]
|
plugins = ["setuptools"]
|
||||||
requirements_deprecated_finder = ["pip-api", "pipreqs"]
|
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]]
|
[[package]]
|
||||||
name = "mccabe"
|
name = "mccabe"
|
||||||
version = "0.7.0"
|
version = "0.7.0"
|
||||||
|
@ -296,6 +419,28 @@ pluggy = ">=0.12,<2.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "xmlschema"]
|
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]]
|
[[package]]
|
||||||
name = "snowballstemmer"
|
name = "snowballstemmer"
|
||||||
version = "2.2.0"
|
version = "2.2.0"
|
||||||
|
@ -304,6 +449,20 @@ category = "dev"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = "*"
|
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]]
|
[[package]]
|
||||||
name = "typing-extensions"
|
name = "typing-extensions"
|
||||||
version = "4.4.0"
|
version = "4.4.0"
|
||||||
|
@ -312,12 +471,42 @@ category = "main"
|
||||||
optional = false
|
optional = false
|
||||||
python-versions = ">=3.7"
|
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]
|
[metadata]
|
||||||
lock-version = "1.1"
|
lock-version = "1.1"
|
||||||
python-versions = ">=3.11,<3.12"
|
python-versions = ">=3.11,<3.12"
|
||||||
content-hash = "0de1b1934bd8af99bf38ce61df2b14773e345daa43c6cb1917bb5b282e4b1e65"
|
content-hash = "13bef4122cae8394c862b2497c696d777b88604803563af278fbbfdc0d7c3b78"
|
||||||
|
|
||||||
[metadata.files]
|
[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 = [
|
attrs = [
|
||||||
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
{file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"},
|
||||||
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
|
{file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"},
|
||||||
|
@ -345,6 +534,10 @@ black = [
|
||||||
{file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
|
{file = "black-22.10.0-py3-none-any.whl", hash = "sha256:c957b2b4ea88587b46cf49d1dc17681c1e672864fd7af32fc1e9664d572b3458"},
|
||||||
{file = "black-22.10.0.tar.gz", hash = "sha256:f513588da599943e0cde4e32cc9879e825d58720d6557062d1098c5ad80080e1"},
|
{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 = [
|
click = [
|
||||||
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
{file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"},
|
||||||
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
{file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"},
|
||||||
|
@ -353,10 +546,66 @@ colorama = [
|
||||||
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
{file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"},
|
||||||
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
{file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"},
|
||||||
]
|
]
|
||||||
|
coverage = [
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ef8674b0ee8cc11e2d574e3e2998aea5df5ab242e012286824ea3c6970580e53"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:784f53ebc9f3fd0e2a3f6a78b2be1bd1f5575d7863e10c6e12504f240fd06660"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b4a5be1748d538a710f87542f22c2cad22f80545a847ad91ce45e77417293eb4"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83516205e254a0cb77d2d7bb3632ee019d93d9f4005de31dca0a8c3667d5bc04"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af4fffaffc4067232253715065e30c5a7ec6faac36f8fc8d6f64263b15f74db0"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:97117225cdd992a9c2a5515db1f66b59db634f59d0679ca1fa3fe8da32749cae"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:a1170fa54185845505fbfa672f1c1ab175446c887cce8212c44149581cf2d466"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:11b990d520ea75e7ee8dcab5bc908072aaada194a794db9f6d7d5cfd19661e5a"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-win32.whl", hash = "sha256:5dbec3b9095749390c09ab7c89d314727f18800060d8d24e87f01fb9cfb40b32"},
|
||||||
|
{file = "coverage-6.5.0-cp310-cp310-win_amd64.whl", hash = "sha256:59f53f1dc5b656cafb1badd0feb428c1e7bc19b867479ff72f7a9dd9b479f10e"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4a5375e28c5191ac38cca59b38edd33ef4cc914732c916f2929029b4bfb50795"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c4ed2820d919351f4167e52425e096af41bfabacb1857186c1ea32ff9983ed75"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:33a7da4376d5977fbf0a8ed91c4dffaaa8dbf0ddbf4c8eea500a2486d8bc4d7b"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a8fb6cf131ac4070c9c5a3e21de0f7dc5a0fbe8bc77c9456ced896c12fcdad91"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:a6b7d95969b8845250586f269e81e5dfdd8ff828ddeb8567a4a2eaa7313460c4"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:1ef221513e6f68b69ee9e159506d583d31aa3567e0ae84eaad9d6ec1107dddaa"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:cca4435eebea7962a52bdb216dec27215d0df64cf27fc1dd538415f5d2b9da6b"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-win32.whl", hash = "sha256:98e8a10b7a314f454d9eff4216a9a94d143a7ee65018dd12442e898ee2310578"},
|
||||||
|
{file = "coverage-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:bc8ef5e043a2af066fa8cbfc6e708d58017024dc4345a1f9757b329a249f041b"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:4433b90fae13f86fafff0b326453dd42fc9a639a0d9e4eec4d366436d1a41b6d"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4f05d88d9a80ad3cac6244d36dd89a3c00abc16371769f1340101d3cb899fc3"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:94e2565443291bd778421856bc975d351738963071e9b8839ca1fc08b42d4bef"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:027018943386e7b942fa832372ebc120155fd970837489896099f5cfa2890f79"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:255758a1e3b61db372ec2736c8e2a1fdfaf563977eedbdf131de003ca5779b7d"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:851cf4ff24062c6aec510a454b2584f6e998cada52d4cb58c5e233d07172e50c"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:12adf310e4aafddc58afdb04d686795f33f4d7a6fa67a7a9d4ce7d6ae24d949f"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-win32.whl", hash = "sha256:b5604380f3415ba69de87a289a2b56687faa4fe04dbee0754bfcae433489316b"},
|
||||||
|
{file = "coverage-6.5.0-cp37-cp37m-win_amd64.whl", hash = "sha256:4a8dbc1f0fbb2ae3de73eb0bdbb914180c7abfbf258e90b311dcd4f585d44bd2"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:d900bb429fdfd7f511f868cedd03a6bbb142f3f9118c09b99ef8dc9bf9643c3c"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:2198ea6fc548de52adc826f62cb18554caedfb1d26548c1b7c88d8f7faa8f6ba"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c4459b3de97b75e3bd6b7d4b7f0db13f17f504f3d13e2a7c623786289dd670e"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:20c8ac5386253717e5ccc827caad43ed66fea0efe255727b1053a8154d952398"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b07130585d54fe8dff3d97b93b0e20290de974dc8177c320aeaf23459219c0b"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:dbdb91cd8c048c2b09eb17713b0c12a54fbd587d79adcebad543bc0cd9a3410b"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:de3001a203182842a4630e7b8d1a2c7c07ec1b45d3084a83d5d227a3806f530f"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e07f4a4a9b41583d6eabec04f8b68076ab3cd44c20bd29332c6572dda36f372e"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-win32.whl", hash = "sha256:6d4817234349a80dbf03640cec6109cd90cba068330703fa65ddf56b60223a6d"},
|
||||||
|
{file = "coverage-6.5.0-cp38-cp38-win_amd64.whl", hash = "sha256:7ccf362abd726b0410bf8911c31fbf97f09f8f1061f8c1cf03dfc4b6372848f6"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:633713d70ad6bfc49b34ead4060531658dc6dfc9b3eb7d8a716d5873377ab745"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:95203854f974e07af96358c0b261f1048d8e1083f2de9b1c565e1be4a3a48cfc"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b9023e237f4c02ff739581ef35969c3739445fb059b060ca51771e69101efffe"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:265de0fa6778d07de30bcf4d9dc471c3dc4314a23a3c6603d356a3c9abc2dfcf"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f830ed581b45b82451a40faabb89c84e1a998124ee4212d440e9c6cf70083e5"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7b6be138d61e458e18d8e6ddcddd36dd96215edfe5f1168de0b1b32635839b62"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:42eafe6778551cf006a7c43153af1211c3aaab658d4d66fa5fcc021613d02518"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:723e8130d4ecc8f56e9a611e73b31219595baa3bb252d539206f7bbbab6ffc1f"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-win32.whl", hash = "sha256:d9ecf0829c6a62b9b573c7bb6d4dcd6ba8b6f80be9ba4fc7ed50bf4ac9aecd72"},
|
||||||
|
{file = "coverage-6.5.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc2af30ed0d5ae0b1abdb4ebdce598eafd5b35397d4d75deb341a614d333d987"},
|
||||||
|
{file = "coverage-6.5.0-pp36.pp37.pp38-none-any.whl", hash = "sha256:1431986dac3923c5945271f169f59c45b8802a114c8f548d611f2015133df77a"},
|
||||||
|
{file = "coverage-6.5.0.tar.gz", hash = "sha256:f642e90754ee3e06b0e7e51bce3379590e76b7f76b708e1a71ff043f87025c84"},
|
||||||
|
]
|
||||||
eradicate = [
|
eradicate = [
|
||||||
{file = "eradicate-2.1.0-py3-none-any.whl", hash = "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2"},
|
{file = "eradicate-2.1.0-py3-none-any.whl", hash = "sha256:8bfaca181db9227dc88bdbce4d051a9627604c2243e7d85324f6d6ce0fd08bb2"},
|
||||||
{file = "eradicate-2.1.0.tar.gz", hash = "sha256:aac7384ab25b1bf21c4c012de9b4bf8398945a14c98c911545b2ea50ab558014"},
|
{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 = [
|
flake8 = [
|
||||||
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
|
{file = "flake8-5.0.4-py2.py3-none-any.whl", hash = "sha256:7a1cf6b73744f5806ab95e526f6f0d8c01c66d7bbe349562d22dfca20610b248"},
|
||||||
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
|
{file = "flake8-5.0.4.tar.gz", hash = "sha256:6fbe320aad8d6b95cec8b8e47bc933004678dc63095be98528b7bdd2a9f510db"},
|
||||||
|
@ -375,6 +624,22 @@ flake8-eradicate = [
|
||||||
flake8-use-fstring = [
|
flake8-use-fstring = [
|
||||||
{file = "flake8-use-fstring-1.4.tar.gz", hash = "sha256:6550bf722585eb97dffa8343b0f1c372101f5c4ab5b07ebf0edd1c79880cdd39"},
|
{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 = [
|
iniconfig = [
|
||||||
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
{file = "iniconfig-1.1.1-py2.py3-none-any.whl", hash = "sha256:011e24c64b7f47f6ebd835bb12a743f2fbe9a26d4cecaa7f53bc4f35ee9da8b3"},
|
||||||
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
{file = "iniconfig-1.1.1.tar.gz", hash = "sha256:bc3af051d7d14b2ee5ef9969666def0cd1a000e121eaea580d4a313df4b37f32"},
|
||||||
|
@ -383,6 +648,10 @@ isort = [
|
||||||
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
{file = "isort-5.10.1-py3-none-any.whl", hash = "sha256:6f62d78e2f89b4500b080fe3a81690850cd254227f27f75c3a0c491a1f351ba7"},
|
||||||
{file = "isort-5.10.1.tar.gz", hash = "sha256:e8443a5e7a020e9d7f97f1d7d9cd17c88bcb3bc7e218bf9cf5095fe550be2951"},
|
{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 = [
|
mccabe = [
|
||||||
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
{file = "mccabe-0.7.0-py2.py3-none-any.whl", hash = "sha256:6c2d30ab6be0e4a46919781807b4f0d834ebdd6c6e3dca0bda5a15f863427b6e"},
|
||||||
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
{file = "mccabe-0.7.0.tar.gz", hash = "sha256:348e0240c33b60bbdf4e523192ef919f28cb2c3d7d5c7794f74009290f236325"},
|
||||||
|
@ -497,11 +766,31 @@ pytest = [
|
||||||
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
{file = "pytest-7.2.0-py3-none-any.whl", hash = "sha256:892f933d339f068883b6fd5a459f03d85bfcb355e4981e146d2c7616c21fef71"},
|
||||||
{file = "pytest-7.2.0.tar.gz", hash = "sha256:c4014eb40e10f11f355ad4e3c2fb2c6c6d1919c73f3b5a433de4708202cade59"},
|
{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 = [
|
snowballstemmer = [
|
||||||
{file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
|
{file = "snowballstemmer-2.2.0-py2.py3-none-any.whl", hash = "sha256:c8e1716e83cc398ae16824e5572ae04e0d9fc2c6b985fb0f900f5f0c96ecba1a"},
|
||||||
{file = "snowballstemmer-2.2.0.tar.gz", hash = "sha256:09b16deb8547d3412ad7b590689584cd0fe25ec8db3be37788be3810cbf19cb1"},
|
{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 = [
|
typing-extensions = [
|
||||||
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
{file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"},
|
||||||
{file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"},
|
{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"},
|
||||||
|
]
|
||||||
|
|
|
@ -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
|
|
@ -3,19 +3,38 @@ from enum import IntEnum, auto
|
||||||
from poker.utils.enum import AutoName
|
from poker.utils.enum import AutoName
|
||||||
|
|
||||||
|
|
||||||
|
class Rank(IntEnum):
|
||||||
|
"""Poker rank enum."""
|
||||||
|
|
||||||
|
ROYAL_FLUSH = 1
|
||||||
|
STRAIGHT_FLUSH = 2
|
||||||
|
FOUR_OF_A_KIND = 3
|
||||||
|
FULL_HOUSE = 4
|
||||||
|
FLUSH = 5
|
||||||
|
STRAIGHT = 6
|
||||||
|
THREE_OF_A_KIND = 7
|
||||||
|
TWO_PAIR = 8
|
||||||
|
PAIR = 9
|
||||||
|
HIGH_CARD = 10
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return string representation."""
|
||||||
|
out: str = self.name.lower().replace("_", " ").title()
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
class Suit(AutoName):
|
class Suit(AutoName):
|
||||||
"""Card suit enum."""
|
"""Card suit enum."""
|
||||||
|
|
||||||
CLUBS: auto()
|
CLUBS = auto()
|
||||||
DIAMONDS: auto()
|
DIAMONDS = auto()
|
||||||
HEARTS: auto()
|
HEARTS = auto()
|
||||||
SPADES: auto()
|
SPADES = auto()
|
||||||
|
|
||||||
|
|
||||||
class Value(IntEnum):
|
class Value(IntEnum):
|
||||||
"""Card value enum."""
|
"""Card value enum."""
|
||||||
|
|
||||||
ACE = 1
|
|
||||||
TWO = 2
|
TWO = 2
|
||||||
THREE = 3
|
THREE = 3
|
||||||
FOUR = 4
|
FOUR = 4
|
||||||
|
@ -28,3 +47,12 @@ class Value(IntEnum):
|
||||||
JACK = 11
|
JACK = 11
|
||||||
QUEEN = 12
|
QUEEN = 12
|
||||||
KING = 13
|
KING = 13
|
||||||
|
ACE = 14
|
||||||
|
|
||||||
|
def __str__(self) -> str:
|
||||||
|
"""Return string representation."""
|
||||||
|
if self.value in [1, 11, 12, 13, 14]:
|
||||||
|
out: str = self.name.lower()
|
||||||
|
else:
|
||||||
|
out = str(self.value)
|
||||||
|
return out
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
from pydantic import BaseModel, Field
|
from __future__ import annotations
|
||||||
|
|
||||||
from poker.constants import Suit, Value
|
from collections import Counter
|
||||||
|
|
||||||
|
from pydantic import BaseModel, validator
|
||||||
|
|
||||||
|
from poker.constants import Rank, Suit, Value
|
||||||
|
|
||||||
|
|
||||||
class Card(BaseModel):
|
class Card(BaseModel):
|
||||||
|
@ -9,8 +13,46 @@ class Card(BaseModel):
|
||||||
suit: Suit
|
suit: Suit
|
||||||
value: Value
|
value: Value
|
||||||
|
|
||||||
|
def __hash__(self) -> int:
|
||||||
|
"""Hash function."""
|
||||||
|
return hash(f"{self.value} {self.suit}")
|
||||||
|
|
||||||
|
def __le__(self, other: Card) -> bool:
|
||||||
|
"""Less than or equal to."""
|
||||||
|
return self.value <= other.value
|
||||||
|
|
||||||
|
def __lt__(self, other: Card) -> bool:
|
||||||
|
"""Strictly less than."""
|
||||||
|
return self.value < other.value
|
||||||
|
|
||||||
|
|
||||||
class Hand(BaseModel):
|
class Hand(BaseModel):
|
||||||
"""Hand domain model class."""
|
"""Hand domain model class."""
|
||||||
|
|
||||||
cards: list[Card] = Field(..., min_length=5, max_length=5)
|
cards: list[Card]
|
||||||
|
|
||||||
|
@validator("cards")
|
||||||
|
def validate_unique(cls, cards: list[Card]) -> list[Card]:
|
||||||
|
"""Validate hand comprises unique cards."""
|
||||||
|
if len(cards) != len(set(cards)):
|
||||||
|
raise ValueError("Hand contains duplicate cards.")
|
||||||
|
return cards
|
||||||
|
|
||||||
|
@validator("cards")
|
||||||
|
def validate_length(cls, cards: list[Card]) -> list[Card]:
|
||||||
|
"""Validate hand has five cards."""
|
||||||
|
if len(cards) != 5:
|
||||||
|
raise ValueError("Hand must have five cards.")
|
||||||
|
return cards
|
||||||
|
|
||||||
|
@property
|
||||||
|
def value_counts(self) -> list[tuple[Value, int]]:
|
||||||
|
"""Return count of each card value in hand."""
|
||||||
|
return Counter([card.value for card in self.cards]).most_common()
|
||||||
|
|
||||||
|
|
||||||
|
class RankedHand(Hand):
|
||||||
|
"""Ranked hand domain model class."""
|
||||||
|
|
||||||
|
rank: Rank
|
||||||
|
description: str
|
||||||
|
|
|
@ -0,0 +1,3 @@
|
||||||
|
from poker.rank.hands import rank_hand
|
||||||
|
|
||||||
|
__all__ = ["rank_hand"]
|
|
@ -0,0 +1,14 @@
|
||||||
|
from poker.constants import Rank
|
||||||
|
|
||||||
|
DESCRIPTIONS = {
|
||||||
|
Rank.ROYAL_FLUSH: "royal flush: {suit}",
|
||||||
|
Rank.STRAIGHT_FLUSH: "straight flush: {high}-high {suit}",
|
||||||
|
Rank.FOUR_OF_A_KIND: "four of a kind: {value}",
|
||||||
|
Rank.FULL_HOUSE: "full house: {trips} over {pair}",
|
||||||
|
Rank.FLUSH: "flush: {suit}",
|
||||||
|
Rank.STRAIGHT: "straight: {high}-high",
|
||||||
|
Rank.THREE_OF_A_KIND: "three of a kind: {value}",
|
||||||
|
Rank.TWO_PAIR: "two pair: {high} and {low}",
|
||||||
|
Rank.PAIR: "pair: {value}",
|
||||||
|
Rank.HIGH_CARD: "high card: {value}",
|
||||||
|
}
|
|
@ -0,0 +1,172 @@
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
from poker.constants import Rank, Value
|
||||||
|
from poker.models import Card, Hand, RankedHand
|
||||||
|
from poker.rank.descriptions import DESCRIPTIONS
|
||||||
|
|
||||||
|
|
||||||
|
def _is_flush(cards: list[Card]) -> bool:
|
||||||
|
"""Calculate whether hand is a flush."""
|
||||||
|
return len(set(card.suit for card in cards)) == 1
|
||||||
|
|
||||||
|
|
||||||
|
def _is_straight(cards: list[Card]) -> Optional[Value]:
|
||||||
|
"""Calculate whether hand is a straight.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
cards: List of cards in hand.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
High card if hand is a straight, None otherwise.
|
||||||
|
"""
|
||||||
|
card_values = sorted([card.value for card in cards])
|
||||||
|
|
||||||
|
# If card values are not unique it is not a straight
|
||||||
|
if len(set(card_values)) != len(card_values):
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Check for a ace low straight
|
||||||
|
if card_values == [Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.ACE]:
|
||||||
|
return Value.FIVE
|
||||||
|
|
||||||
|
# If all card differences are 1 it is a straight
|
||||||
|
diffs = [second - first for first, second in zip(card_values[:-1], card_values[1:])]
|
||||||
|
if set(diffs) == {1}:
|
||||||
|
return max(cards).value
|
||||||
|
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def royal_flush(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand a royal flush."""
|
||||||
|
if not _is_flush(hand.cards):
|
||||||
|
return {}
|
||||||
|
|
||||||
|
values = [card.value for card in hand.cards]
|
||||||
|
_is = all(
|
||||||
|
value in values
|
||||||
|
for value in [Value.ACE, Value.KING, Value.QUEEN, Value.JACK, Value.TEN]
|
||||||
|
)
|
||||||
|
if _is:
|
||||||
|
return {"suit": hand.cards[0].suit}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def straight_flush(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand a straight flush."""
|
||||||
|
high = _is_straight(hand.cards)
|
||||||
|
if high is not None and _is_flush(hand.cards):
|
||||||
|
return {
|
||||||
|
"high": high,
|
||||||
|
"suit": hand.cards[0].suit,
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def four_of_a_kind(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand has four of a kind."""
|
||||||
|
if hand.value_counts[0][1] == 4:
|
||||||
|
return {"value": hand.value_counts[0][0]}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def full_house(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand is a full house."""
|
||||||
|
if hand.value_counts[0][1] == 3 and hand.value_counts[1][1] == 2:
|
||||||
|
return {
|
||||||
|
"trips": hand.value_counts[0][0],
|
||||||
|
"pair": hand.value_counts[1][0],
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def flush(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand is a flush."""
|
||||||
|
high = _is_straight(hand.cards)
|
||||||
|
if _is_flush(hand.cards) and high is None:
|
||||||
|
return {"suit": hand.cards[0].suit}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def straight(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand is a flush."""
|
||||||
|
high = _is_straight(hand.cards)
|
||||||
|
if high is not None and not _is_flush(hand.cards):
|
||||||
|
return {"high": high}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def three_of_a_kind(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand is a full house."""
|
||||||
|
if (
|
||||||
|
hand.value_counts[0][1] == 3
|
||||||
|
and hand.value_counts[1][1] == 1
|
||||||
|
and hand.value_counts[2][1] == 1
|
||||||
|
):
|
||||||
|
return {"value": hand.value_counts[0][0]}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def two_pair(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand is a two pair."""
|
||||||
|
if hand.value_counts[0][1] == 2 and hand.value_counts[1][1] == 2:
|
||||||
|
values = [hand.value_counts[0][0], hand.value_counts[1][0]]
|
||||||
|
return {
|
||||||
|
"high": max(values),
|
||||||
|
"low": min(values),
|
||||||
|
}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def pair(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Calculate whether hand is a pair."""
|
||||||
|
if hand.value_counts[0][1] == 2 and hand.value_counts[1][1] == 1:
|
||||||
|
return {"value": hand.value_counts[0][0]}
|
||||||
|
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
def high_card(hand: Hand) -> dict[str, Union[str, int]]:
|
||||||
|
"""Get high card."""
|
||||||
|
return {"value": max(hand.cards).value}
|
||||||
|
|
||||||
|
|
||||||
|
_FUNCTIONS = {
|
||||||
|
Rank.ROYAL_FLUSH: royal_flush,
|
||||||
|
Rank.STRAIGHT_FLUSH: straight_flush,
|
||||||
|
Rank.FOUR_OF_A_KIND: four_of_a_kind,
|
||||||
|
Rank.FULL_HOUSE: full_house,
|
||||||
|
Rank.FLUSH: flush,
|
||||||
|
Rank.STRAIGHT: straight,
|
||||||
|
Rank.THREE_OF_A_KIND: three_of_a_kind,
|
||||||
|
Rank.TWO_PAIR: two_pair,
|
||||||
|
Rank.PAIR: pair,
|
||||||
|
Rank.HIGH_CARD: high_card,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def rank_hand(hand: Hand) -> RankedHand:
|
||||||
|
"""Get hand rank.
|
||||||
|
|
||||||
|
TODO: Use a factory pattern to avoid this huge flow control.
|
||||||
|
"""
|
||||||
|
out = None
|
||||||
|
for rank, func in _FUNCTIONS.items():
|
||||||
|
result = func(hand)
|
||||||
|
if result:
|
||||||
|
return RankedHand(
|
||||||
|
cards=hand.cards,
|
||||||
|
rank=rank,
|
||||||
|
description=DESCRIPTIONS[rank].format(**result),
|
||||||
|
)
|
||||||
|
|
||||||
|
if out is None:
|
||||||
|
raise ValueError("No rank found.")
|
|
@ -1,26 +1,31 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "poker"
|
name = "poker"
|
||||||
version = "0.2.3"
|
version = "2.0.0"
|
||||||
description = "Single poker hand ranking service."
|
description = "Single poker hand ranking service."
|
||||||
authors = ["Paul Harrison"]
|
authors = ["Paul Harrison"]
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = ">=3.11,<3.12"
|
python = ">=3.11,<3.12"
|
||||||
|
fastapi = "^0.87.0"
|
||||||
|
loguru = "^0.6.0"
|
||||||
pydantic = "^1.10.2"
|
pydantic = "^1.10.2"
|
||||||
|
uvicorn = "^0.20.0"
|
||||||
|
|
||||||
[tool.poetry.group.test.dependencies]
|
[tool.poetry.group.test.dependencies]
|
||||||
|
coverage = "^6.5.0"
|
||||||
pytest = "^7.2.0"
|
pytest = "^7.2.0"
|
||||||
|
httpx = "^0.23.1"
|
||||||
|
|
||||||
|
|
||||||
[tool.poetry.group.lint.dependencies]
|
[tool.poetry.group.lint.dependencies]
|
||||||
black = "^22.10.0"
|
black = "^22.10.0"
|
||||||
isort = "^5.10.1"
|
|
||||||
flake8 = "^5.0.4"
|
flake8 = "^5.0.4"
|
||||||
flake8-builtins = "^2.0.1"
|
flake8-builtins = "^2.0.1"
|
||||||
flake8-blind-except = "^0.2.1"
|
flake8-blind-except = "^0.2.1"
|
||||||
flake8-eradicate = "^1.4.0"
|
flake8-eradicate = "^1.4.0"
|
||||||
flake8-use-fstring = "^1.4"
|
flake8-use-fstring = "^1.4"
|
||||||
|
isort = "^5.10.1"
|
||||||
mypy = "^0.991"
|
mypy = "^0.991"
|
||||||
pydocstyle = "^6.1.1"
|
pydocstyle = "^6.1.1"
|
||||||
|
|
||||||
|
|
|
@ -1,3 +1,12 @@
|
||||||
|
[coverage:run]
|
||||||
|
data_file = tests/.coverage
|
||||||
|
relative_files = True
|
||||||
|
|
||||||
|
[coverage:report]
|
||||||
|
precision = 1
|
||||||
|
skip_empty = True
|
||||||
|
show_missing = True
|
||||||
|
|
||||||
[flake8]
|
[flake8]
|
||||||
max-line-length = 88
|
max-line-length = 88
|
||||||
ignore =
|
ignore =
|
||||||
|
|
|
@ -0,0 +1,24 @@
|
||||||
|
from typing import Any, Callable, TypeVar
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from mypy_extensions import KwArg
|
||||||
|
|
||||||
|
from poker.constants import Suit, Value
|
||||||
|
from poker.models import Card
|
||||||
|
|
||||||
|
T = TypeVar("T")
|
||||||
|
Factory = Callable[[KwArg(Any)], T]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(scope="session")
|
||||||
|
def card_factory() -> Factory[Card]:
|
||||||
|
"""Get Card object factory."""
|
||||||
|
default = {
|
||||||
|
"suit": Suit.CLUBS,
|
||||||
|
"value": Value.ACE,
|
||||||
|
}
|
||||||
|
|
||||||
|
def factory(**kwargs: Any) -> Card:
|
||||||
|
return Card(**(default | kwargs))
|
||||||
|
|
||||||
|
return factory
|
|
@ -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
|
|
@ -0,0 +1,21 @@
|
||||||
|
import pytest
|
||||||
|
from pydantic import ValidationError
|
||||||
|
|
||||||
|
from poker.constants import Value
|
||||||
|
from poker.models import Card, Hand
|
||||||
|
from tests.conftest import Factory
|
||||||
|
|
||||||
|
|
||||||
|
def test_hand_should_contain_unique_cards(card_factory: Factory[Card]) -> None:
|
||||||
|
cards = [
|
||||||
|
Value.ACE,
|
||||||
|
Value.TWO,
|
||||||
|
Value.THREE,
|
||||||
|
Value.FOUR,
|
||||||
|
]
|
||||||
|
with pytest.raises(ValidationError):
|
||||||
|
_ = Hand(
|
||||||
|
cards=tuple(card_factory(value=value) for value in cards + [Value.FOUR])
|
||||||
|
)
|
||||||
|
|
||||||
|
_ = Hand(cards=tuple(card_factory(value=value) for value in cards + [Value.FIVE]))
|
|
@ -0,0 +1,442 @@
|
||||||
|
from itertools import cycle
|
||||||
|
from typing import Optional, Union
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from poker.constants import Suit, Value
|
||||||
|
from poker.models import Card, Hand
|
||||||
|
from poker.rank.hands import (
|
||||||
|
_is_flush,
|
||||||
|
_is_straight,
|
||||||
|
flush,
|
||||||
|
four_of_a_kind,
|
||||||
|
full_house,
|
||||||
|
high_card,
|
||||||
|
pair,
|
||||||
|
royal_flush,
|
||||||
|
straight,
|
||||||
|
straight_flush,
|
||||||
|
three_of_a_kind,
|
||||||
|
two_pair,
|
||||||
|
)
|
||||||
|
from tests.conftest import Factory
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"suits,expected",
|
||||||
|
[
|
||||||
|
([Suit.CLUBS, Suit.DIAMONDS, Suit.SPADES, Suit.HEARTS, Suit.CLUBS], False),
|
||||||
|
([Suit.CLUBS, Suit.CLUBS, Suit.CLUBS, Suit.CLUBS, Suit.CLUBS], True),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"non-unique",
|
||||||
|
"unique",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test__is_flush(
|
||||||
|
card_factory: Factory[Card], suits: list[Suit], expected: dict[str, Union[str, int]]
|
||||||
|
) -> None:
|
||||||
|
result = _is_flush([card_factory(suit=suit) for suit in suits])
|
||||||
|
|
||||||
|
if expected:
|
||||||
|
assert result
|
||||||
|
else:
|
||||||
|
assert not result
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"values,expected",
|
||||||
|
[
|
||||||
|
([Value.TWO, Value.TWO, Value.THREE, Value.FOUR, Value.FIVE], None),
|
||||||
|
([Value.THREE, Value.TWO, Value.ACE, Value.TEN, Value.KING], None),
|
||||||
|
([Value.ACE, Value.TWO, Value.THREE, Value.FOUR, Value.FIVE], Value.FIVE),
|
||||||
|
([Value.ACE, Value.KING, Value.TEN, Value.JACK, Value.QUEEN], Value.ACE),
|
||||||
|
([Value.THREE, Value.FIVE, Value.TWO, Value.FOUR, Value.SIX], Value.SIX),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"non-unique",
|
||||||
|
"not-flush",
|
||||||
|
"low-ace",
|
||||||
|
"high-ace",
|
||||||
|
"middle-flush",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test__is_straight(
|
||||||
|
card_factory: Factory[Card], values: list[Value], expected: Optional[Value]
|
||||||
|
) -> None:
|
||||||
|
suits = cycle(Suit)
|
||||||
|
result = _is_straight(
|
||||||
|
[card_factory(suit=suit, value=value) for suit, value in zip(suits, values)]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"values,different_suits,expected",
|
||||||
|
[
|
||||||
|
([Value.TWO, Value.TEN, Value.THREE, Value.FOUR, Value.FIVE], False, {}),
|
||||||
|
(
|
||||||
|
[Value.ACE, Value.KING, Value.TEN, Value.JACK, Value.QUEEN],
|
||||||
|
False,
|
||||||
|
{"suit": Suit.CLUBS},
|
||||||
|
),
|
||||||
|
([Value.ACE, Value.KING, Value.TEN, Value.JACK, Value.QUEEN], True, {}),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"not-flush",
|
||||||
|
"royal-flush",
|
||||||
|
"flush-not-royal",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_royal_flush(
|
||||||
|
card_factory: Factory[Card],
|
||||||
|
values: list[Value],
|
||||||
|
different_suits: bool,
|
||||||
|
expected: dict[str, Union[str, int]],
|
||||||
|
) -> None:
|
||||||
|
suits = cycle(Suit)
|
||||||
|
|
||||||
|
if different_suits:
|
||||||
|
result = royal_flush(
|
||||||
|
Hand(
|
||||||
|
cards=[
|
||||||
|
card_factory(suit=suit, value=value)
|
||||||
|
for suit, value in zip(suits, values)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = royal_flush(
|
||||||
|
Hand(cards=[card_factory(value=value) for value in values])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"values,different_suits,expected",
|
||||||
|
[
|
||||||
|
([Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.SIX], True, {}),
|
||||||
|
([Value.TWO, Value.THREE, Value.FIVE, Value.SIX, Value.TEN], False, {}),
|
||||||
|
(
|
||||||
|
[Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.SIX],
|
||||||
|
False,
|
||||||
|
{"high": Value.SIX, "suit": Suit.CLUBS},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"straight-only",
|
||||||
|
"flush-only",
|
||||||
|
"straight-flush",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_straight_flush(
|
||||||
|
card_factory: Factory[Card],
|
||||||
|
values: list[Value],
|
||||||
|
different_suits: bool,
|
||||||
|
expected: dict[str, Union[str, int]],
|
||||||
|
) -> None:
|
||||||
|
suits = cycle(Suit)
|
||||||
|
|
||||||
|
if different_suits:
|
||||||
|
result = straight_flush(
|
||||||
|
Hand(
|
||||||
|
cards=[
|
||||||
|
card_factory(suit=suit, value=value)
|
||||||
|
for suit, value in zip(suits, values)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = straight_flush(
|
||||||
|
Hand(cards=[card_factory(value=value) for value in values])
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cards,expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.TWO),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{"value": Value.TWO},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TEN),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.TWO),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"four-of-a-kind",
|
||||||
|
"not-four-of-a-kind",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_four_of_a_kind(
|
||||||
|
cards: list[Card], expected: dict[str, Union[str, int]]
|
||||||
|
) -> None:
|
||||||
|
result = four_of_a_kind(Hand(cards=cards))
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cards,expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{"trips": Value.TWO, "pair": Value.THREE},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.TWO),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"full-house",
|
||||||
|
"not-full-house",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_full_house(cards: list[Card], expected: dict[str, Union[str, int]]) -> None:
|
||||||
|
result = full_house(Hand(cards=cards))
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"values,different_suits,expected",
|
||||||
|
[
|
||||||
|
([Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.SIX], True, {}),
|
||||||
|
(
|
||||||
|
[Value.TWO, Value.THREE, Value.FIVE, Value.SIX, Value.TEN],
|
||||||
|
False,
|
||||||
|
{"suit": Suit.CLUBS},
|
||||||
|
),
|
||||||
|
([Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.SIX], False, {}),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"straight-only",
|
||||||
|
"flush-only",
|
||||||
|
"straight-flush",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_flush(
|
||||||
|
card_factory: Factory[Card],
|
||||||
|
values: list[Value],
|
||||||
|
different_suits: bool,
|
||||||
|
expected: dict[str, Union[str, int]],
|
||||||
|
) -> None:
|
||||||
|
suits = cycle(Suit)
|
||||||
|
|
||||||
|
if different_suits:
|
||||||
|
result = flush(
|
||||||
|
Hand(
|
||||||
|
cards=[
|
||||||
|
card_factory(suit=suit, value=value)
|
||||||
|
for suit, value in zip(suits, values)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = flush(Hand(cards=[card_factory(value=value) for value in values]))
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"values,different_suits,expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.SIX],
|
||||||
|
True,
|
||||||
|
{"high": Value.SIX},
|
||||||
|
),
|
||||||
|
([Value.TWO, Value.THREE, Value.FIVE, Value.SIX, Value.TEN], False, {}),
|
||||||
|
([Value.TWO, Value.THREE, Value.FOUR, Value.FIVE, Value.SIX], False, {}),
|
||||||
|
],
|
||||||
|
ids=[
|
||||||
|
"straight-only",
|
||||||
|
"flush-only",
|
||||||
|
"straight-flush",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_is_straight(
|
||||||
|
card_factory: Factory[Card],
|
||||||
|
values: list[Value],
|
||||||
|
different_suits: bool,
|
||||||
|
expected: dict[str, Union[str, int]],
|
||||||
|
) -> None:
|
||||||
|
suits = cycle(Suit)
|
||||||
|
|
||||||
|
if different_suits:
|
||||||
|
result = straight(
|
||||||
|
Hand(
|
||||||
|
cards=[
|
||||||
|
card_factory(suit=suit, value=value)
|
||||||
|
for suit, value in zip(suits, values)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
result = straight(Hand(cards=[card_factory(value=value) for value in values]))
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cards,expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.ACE),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{"value": Value.TWO},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TEN),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.ACE),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=["full-house", "three-of-a-kind", "not-three-of-a-kind"],
|
||||||
|
)
|
||||||
|
def test_is_three_of_a_kind(
|
||||||
|
cards: list[Card], expected: dict[str, Union[str, int]]
|
||||||
|
) -> None:
|
||||||
|
result = three_of_a_kind(Hand(cards=cards))
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cards,expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.THREE),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.ACE),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.ACE),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{"high": Value.ACE, "low": Value.TWO},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.ACE),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=["full-house", "two-pair", "three-of-a-kind"],
|
||||||
|
)
|
||||||
|
def test_is_two_pair(cards: list[Card], expected: dict[str, Union[str, int]]) -> None:
|
||||||
|
result = two_pair(Hand(cards=cards))
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"cards,expected",
|
||||||
|
[
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.THREE),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.ACE),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.ACE),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{},
|
||||||
|
),
|
||||||
|
(
|
||||||
|
[
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.DIAMONDS, value=Value.TWO),
|
||||||
|
Card(suit=Suit.HEARTS, value=Value.KING),
|
||||||
|
Card(suit=Suit.SPADES, value=Value.ACE),
|
||||||
|
Card(suit=Suit.CLUBS, value=Value.THREE),
|
||||||
|
],
|
||||||
|
{"value": Value.TWO},
|
||||||
|
),
|
||||||
|
],
|
||||||
|
ids=["full-house", "two-pair", "pair"],
|
||||||
|
)
|
||||||
|
def test_is_pair(cards: list[Card], expected: dict[str, Union[str, int]]) -> None:
|
||||||
|
result = pair(Hand(cards=cards))
|
||||||
|
|
||||||
|
assert result == expected
|
||||||
|
|
||||||
|
|
||||||
|
def test_high_card(card_factory: Factory[Card]) -> None:
|
||||||
|
values = [Value.TWO, Value.THREE, Value.FIVE, Value.SIX, Value.TEN]
|
||||||
|
result = high_card(Hand(cards=[card_factory(value=value) for value in values]))
|
||||||
|
assert result == {"value": Value.TEN}
|
Loading…
Reference in New Issue