From 7d943e9736b0ab6cf447a4463c56cf9937c51f84 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Fri, 16 Feb 2024 13:20:36 +0000 Subject: [PATCH 01/21] add development environment of devcontainer. --- .../yukikawamura/.devcontainer/Dockerfile | 45 +++++++++++++++++++ .../.devcontainer/customizeCommand.sh | 3 ++ .../.devcontainer/devcontainer.json | 26 +++++++++++ .../.devcontainer/postCreateCommand.sh | 8 ++++ .../.vscode/yukikawamura.code-workspace | 8 ++++ 5 files changed, 90 insertions(+) create mode 100644 serverside_challenge_1/challenges/yukikawamura/.devcontainer/Dockerfile create mode 100755 serverside_challenge_1/challenges/yukikawamura/.devcontainer/customizeCommand.sh create mode 100644 serverside_challenge_1/challenges/yukikawamura/.devcontainer/devcontainer.json create mode 100755 serverside_challenge_1/challenges/yukikawamura/.devcontainer/postCreateCommand.sh create mode 100644 serverside_challenge_1/challenges/yukikawamura/.vscode/yukikawamura.code-workspace diff --git a/serverside_challenge_1/challenges/yukikawamura/.devcontainer/Dockerfile b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/Dockerfile new file mode 100644 index 000000000..befa5af7f --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/Dockerfile @@ -0,0 +1,45 @@ +FROM mcr.microsoft.com/devcontainers/base:jammy +# FROM mcr.microsoft.com/devcontainers/base:jammy + +ARG DEBIAN_FRONTEND=noninteractive +ARG USER=vscode + +RUN DEBIAN_FRONTEND=noninteractive \ + && apt-get update \ + && apt-get install -y build-essential --no-install-recommends make \ + ca-certificates \ + git \ + libssl-dev \ + zlib1g-dev \ + libbz2-dev \ + libreadline-dev \ + libsqlite3-dev \ + wget \ + curl \ + llvm \ + libncurses5-dev \ + xz-utils \ + tk-dev \ + libxml2-dev \ + libxmlsec1-dev \ + libffi-dev \ + liblzma-dev + +# Python and poetry installation +USER $USER +ARG HOME="/home/$USER" +ARG PYTHON_VERSION=3.10 +# ARG PYTHON_VERSION=3.10 + +ENV PYENV_ROOT="${HOME}/.pyenv" +ENV PATH="${PYENV_ROOT}/shims:${PYENV_ROOT}/bin:${HOME}/.local/bin:$PATH" + +RUN echo "done 0" \ + && curl https://pyenv.run | bash \ + && echo "done 1" \ + && pyenv install ${PYTHON_VERSION} \ + && echo "done 2" \ + && pyenv global ${PYTHON_VERSION} \ + && echo "done 3" \ + && curl -sSL https://install.python-poetry.org | python3 - \ + && poetry config virtualenvs.in-project true \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/.devcontainer/customizeCommand.sh b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/customizeCommand.sh new file mode 100755 index 000000000..fb6df2322 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/customizeCommand.sh @@ -0,0 +1,3 @@ +# /bin/bash + +# customize for freely diff --git a/serverside_challenge_1/challenges/yukikawamura/.devcontainer/devcontainer.json b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/devcontainer.json new file mode 100644 index 000000000..b1b44625d --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/devcontainer.json @@ -0,0 +1,26 @@ +{ + "name": "poetry3-poetry-pyenv", + "build": { + "dockerfile": "Dockerfile" + }, + + // 👇 Features to add to the Dev Container. More info: https://containers.dev/implementors/features. + // "features": {}, + + // 👇 Use 'forwardPorts' to make a list of ports inside the container available locally. + // "forwardPorts": [], + + // 👇 Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "", + + // 👇 Configure tool-specific properties. + "customizations": { + "vscode": { + "extensions":["ms-python.python", "njpwerner.autodocstring"] + } + }, + "postCreateCommand": ".devcontainer/postCreateCommand.sh", + + // 👇 Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + "remoteUser": "root" +} diff --git a/serverside_challenge_1/challenges/yukikawamura/.devcontainer/postCreateCommand.sh b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/postCreateCommand.sh new file mode 100755 index 000000000..4a155f997 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/postCreateCommand.sh @@ -0,0 +1,8 @@ +# /bin/bash + + +# install project +# poetry install --directory=electricity-rate-simulator + +# for developer customized +.devcontainer/customizeCommand.sh diff --git a/serverside_challenge_1/challenges/yukikawamura/.vscode/yukikawamura.code-workspace b/serverside_challenge_1/challenges/yukikawamura/.vscode/yukikawamura.code-workspace new file mode 100644 index 000000000..bab1b7f61 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/.vscode/yukikawamura.code-workspace @@ -0,0 +1,8 @@ +{ + "folders": [ + { + "path": ".." + } + ], + "settings": {} +} \ No newline at end of file From e502c9f4edf26e4cc036573dc2c1ab28834ae2b3 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Fri, 16 Feb 2024 13:27:48 +0000 Subject: [PATCH 02/21] add project --- .../.devcontainer/postCreateCommand.sh | 2 +- .../challenges/yukikawamura/.gitignore | 160 ++++ .../electricity-rate-simulator/README.md | 17 + .../electricity_rate_simulator/__init__.py | 0 .../electricity-rate-simulator/poetry.lock | 698 ++++++++++++++++++ .../electricity-rate-simulator/pyproject.toml | 19 + .../tests/__init__.py | 0 7 files changed, 895 insertions(+), 1 deletion(-) create mode 100644 serverside_challenge_1/challenges/yukikawamura/.gitignore create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/README.md create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/__init__.py create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/__init__.py diff --git a/serverside_challenge_1/challenges/yukikawamura/.devcontainer/postCreateCommand.sh b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/postCreateCommand.sh index 4a155f997..cdba0c7d0 100755 --- a/serverside_challenge_1/challenges/yukikawamura/.devcontainer/postCreateCommand.sh +++ b/serverside_challenge_1/challenges/yukikawamura/.devcontainer/postCreateCommand.sh @@ -2,7 +2,7 @@ # install project -# poetry install --directory=electricity-rate-simulator +poetry install --directory=electricity-rate-simulator # for developer customized .devcontainer/customizeCommand.sh diff --git a/serverside_challenge_1/challenges/yukikawamura/.gitignore b/serverside_challenge_1/challenges/yukikawamura/.gitignore new file mode 100644 index 000000000..68bc17f9f --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/.gitignore @@ -0,0 +1,160 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/README.md b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/README.md new file mode 100644 index 000000000..d22a65860 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/README.md @@ -0,0 +1,17 @@ +# electricity-rate-simulator + +## tl;dr +電気料金をシミュレートするbackend api + + +### usage + + +- request + +- response + + +### for developer + +``` poetry install ``` \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/__init__.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock new file mode 100644 index 000000000..732e10d4a --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock @@ -0,0 +1,698 @@ +# This file is automatically @generated by Poetry 1.7.1 and should not be changed by hand. + +[[package]] +name = "annotated-types" +version = "0.6.0" +description = "Reusable constraint types to use with typing.Annotated" +optional = false +python-versions = ">=3.8" +files = [ + {file = "annotated_types-0.6.0-py3-none-any.whl", hash = "sha256:0641064de18ba7a25dee8f96403ebc39113d0cb953a01429249d5c7564666a43"}, + {file = "annotated_types-0.6.0.tar.gz", hash = "sha256:563339e807e53ffd9c267e99fc6d9ea23eb8443c08f112651963e24e22f84a5d"}, +] + +[[package]] +name = "anyio" +version = "4.2.0" +description = "High level compatibility layer for multiple asynchronous event loop implementations" +optional = false +python-versions = ">=3.8" +files = [ + {file = "anyio-4.2.0-py3-none-any.whl", hash = "sha256:745843b39e829e108e518c489b31dc757de7d2131d53fac32bd8df268227bfee"}, + {file = "anyio-4.2.0.tar.gz", hash = "sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f"}, +] + +[package.dependencies] +exceptiongroup = {version = ">=1.0.2", markers = "python_version < \"3.11\""} +idna = ">=2.8" +sniffio = ">=1.1" +typing-extensions = {version = ">=4.1", markers = "python_version < \"3.11\""} + +[package.extras] +doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphinx-rtd-theme"] +test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] +trio = ["trio (>=0.23)"] + +[[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 = "exceptiongroup" +version = "1.2.0" +description = "Backport of PEP 654 (exception groups)" +optional = false +python-versions = ">=3.7" +files = [ + {file = "exceptiongroup-1.2.0-py3-none-any.whl", hash = "sha256:4bfd3996ac73b41e9b9628b04e079f193850720ea5945fc96a08633c66912f14"}, + {file = "exceptiongroup-1.2.0.tar.gz", hash = "sha256:91f5c769735f051a4290d52edd0858999b57e5876e9f85937691bd4c9fa3ed68"}, +] + +[package.extras] +test = ["pytest (>=6)"] + +[[package]] +name = "fastapi" +version = "0.109.2" +description = "FastAPI framework, high performance, easy to learn, fast to code, ready for production" +optional = false +python-versions = ">=3.8" +files = [ + {file = "fastapi-0.109.2-py3-none-any.whl", hash = "sha256:2c9bab24667293b501cad8dd388c05240c850b58ec5876ee3283c47d6e1e3a4d"}, + {file = "fastapi-0.109.2.tar.gz", hash = "sha256:f3817eac96fe4f65a2ebb4baa000f394e55f5fccdaf7f75250804bc58f354f73"}, +] + +[package.dependencies] +pydantic = ">=1.7.4,<1.8 || >1.8,<1.8.1 || >1.8.1,<2.0.0 || >2.0.0,<2.0.1 || >2.0.1,<2.1.0 || >2.1.0,<3.0.0" +starlette = ">=0.36.3,<0.37.0" +typing-extensions = ">=4.8.0" + +[package.extras] +all = ["email-validator (>=2.0.0)", "httpx (>=0.23.0)", "itsdangerous (>=1.1.0)", "jinja2 (>=2.11.2)", "orjson (>=3.2.1)", "pydantic-extra-types (>=2.0.0)", "pydantic-settings (>=2.0.0)", "python-multipart (>=0.0.7)", "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)"] + +[[package]] +name = "h11" +version = "0.14.0" +description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" +optional = false +python-versions = ">=3.7" +files = [ + {file = "h11-0.14.0-py3-none-any.whl", hash = "sha256:e3fe4ac4b851c468cc8363d500db52c2ead036020723024a109d37346efaa761"}, + {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, +] + +[[package]] +name = "httptools" +version = "0.6.1" +description = "A collection of framework independent HTTP protocol utils." +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d2f6c3c4cb1948d912538217838f6e9960bc4a521d7f9b323b3da579cd14532f"}, + {file = "httptools-0.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:00d5d4b68a717765b1fabfd9ca755bd12bf44105eeb806c03d1962acd9b8e563"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:639dc4f381a870c9ec860ce5c45921db50205a37cc3334e756269736ff0aac58"}, + {file = "httptools-0.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e57997ac7fb7ee43140cc03664de5f268813a481dff6245e0075925adc6aa185"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:0ac5a0ae3d9f4fe004318d64b8a854edd85ab76cffbf7ef5e32920faef62f142"}, + {file = "httptools-0.6.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:3f30d3ce413088a98b9db71c60a6ada2001a08945cb42dd65a9a9fe228627658"}, + {file = "httptools-0.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:1ed99a373e327f0107cb513b61820102ee4f3675656a37a50083eda05dc9541b"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7a7ea483c1a4485c71cb5f38be9db078f8b0e8b4c4dc0210f531cdd2ddac1ef1"}, + {file = "httptools-0.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:85ed077c995e942b6f1b07583e4eb0a8d324d418954fc6af913d36db7c05a5a0"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b0bb634338334385351a1600a73e558ce619af390c2b38386206ac6a27fecfc"}, + {file = "httptools-0.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d9ceb2c957320def533671fc9c715a80c47025139c8d1f3797477decbc6edd2"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4f0f8271c0a4db459f9dc807acd0eadd4839934a4b9b892f6f160e94da309837"}, + {file = "httptools-0.6.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:6a4f5ccead6d18ec072ac0b84420e95d27c1cdf5c9f1bc8fbd8daf86bd94f43d"}, + {file = "httptools-0.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:5cceac09f164bcba55c0500a18fe3c47df29b62353198e4f37bbcc5d591172c3"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:75c8022dca7935cba14741a42744eee13ba05db00b27a4b940f0d646bd4d56d0"}, + {file = "httptools-0.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:48ed8129cd9a0d62cf4d1575fcf90fb37e3ff7d5654d3a5814eb3d55f36478c2"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f58e335a1402fb5a650e271e8c2d03cfa7cea46ae124649346d17bd30d59c90"}, + {file = "httptools-0.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93ad80d7176aa5788902f207a4e79885f0576134695dfb0fefc15b7a4648d503"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:9bb68d3a085c2174c2477eb3ffe84ae9fb4fde8792edb7bcd09a1d8467e30a84"}, + {file = "httptools-0.6.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:b512aa728bc02354e5ac086ce76c3ce635b62f5fbc32ab7082b5e582d27867bb"}, + {file = "httptools-0.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:97662ce7fb196c785344d00d638fc9ad69e18ee4bfb4000b35a52efe5adcc949"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:8e216a038d2d52ea13fdd9b9c9c7459fb80d78302b257828285eca1c773b99b3"}, + {file = "httptools-0.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:3e802e0b2378ade99cd666b5bffb8b2a7cc8f3d28988685dc300469ea8dd86cb"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4bd3e488b447046e386a30f07af05f9b38d3d368d1f7b4d8f7e10af85393db97"}, + {file = "httptools-0.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe467eb086d80217b7584e61313ebadc8d187a4d95bb62031b7bab4b205c3ba3"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3c3b214ce057c54675b00108ac42bacf2ab8f85c58e3f324a4e963bbc46424f4"}, + {file = "httptools-0.6.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:8ae5b97f690badd2ca27cbf668494ee1b6d34cf1c464271ef7bfa9ca6b83ffaf"}, + {file = "httptools-0.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:405784577ba6540fa7d6ff49e37daf104e04f4b4ff2d1ac0469eaa6a20fde084"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:95fb92dd3649f9cb139e9c56604cc2d7c7bf0fc2e7c8d7fbd58f96e35eddd2a3"}, + {file = "httptools-0.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:dcbab042cc3ef272adc11220517278519adf8f53fd3056d0e68f0a6f891ba94e"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cf2372e98406efb42e93bfe10f2948e467edfd792b015f1b4ecd897903d3e8d"}, + {file = "httptools-0.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:678fcbae74477a17d103b7cae78b74800d795d702083867ce160fc202104d0da"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:e0b281cf5a125c35f7f6722b65d8542d2e57331be573e9e88bc8b0115c4a7a81"}, + {file = "httptools-0.6.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:95658c342529bba4e1d3d2b1a874db16c7cca435e8827422154c9da76ac4e13a"}, + {file = "httptools-0.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:7ebaec1bf683e4bf5e9fbb49b8cc36da482033596a415b3e4ebab5a4c0d7ec5e"}, + {file = "httptools-0.6.1.tar.gz", hash = "sha256:c6e26c30455600b95d94b1b836085138e82f177351454ee841c148f93a9bad5a"}, +] + +[package.extras] +test = ["Cython (>=0.29.24,<0.30.0)"] + +[[package]] +name = "idna" +version = "3.6" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.5" +files = [ + {file = "idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f"}, + {file = "idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca"}, +] + +[[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 = "packaging" +version = "23.2" +description = "Core utilities for Python packages" +optional = false +python-versions = ">=3.7" +files = [ + {file = "packaging-23.2-py3-none-any.whl", hash = "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7"}, + {file = "packaging-23.2.tar.gz", hash = "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5"}, +] + +[[package]] +name = "pluggy" +version = "1.4.0" +description = "plugin and hook calling mechanisms for python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pluggy-1.4.0-py3-none-any.whl", hash = "sha256:7db9f7b503d67d1c5b95f59773ebb58a8c1c288129a88665838012cfb07b8981"}, + {file = "pluggy-1.4.0.tar.gz", hash = "sha256:8c85c2876142a764e5b7548e7d9a0e0ddb46f5185161049a79b7e974454223be"}, +] + +[package.extras] +dev = ["pre-commit", "tox"] +testing = ["pytest", "pytest-benchmark"] + +[[package]] +name = "pydantic" +version = "2.6.1" +description = "Data validation using Python type hints" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic-2.6.1-py3-none-any.whl", hash = "sha256:0b6a909df3192245cb736509a92ff69e4fef76116feffec68e93a567347bae6f"}, + {file = "pydantic-2.6.1.tar.gz", hash = "sha256:4fd5c182a2488dc63e6d32737ff19937888001e2a6d86e94b3f233104a5d1fa9"}, +] + +[package.dependencies] +annotated-types = ">=0.4.0" +pydantic-core = "2.16.2" +typing-extensions = ">=4.6.1" + +[package.extras] +email = ["email-validator (>=2.0.0)"] + +[[package]] +name = "pydantic-core" +version = "2.16.2" +description = "" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3fab4e75b8c525a4776e7630b9ee48aea50107fea6ca9f593c98da3f4d11bf7c"}, + {file = "pydantic_core-2.16.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8bde5b48c65b8e807409e6f20baee5d2cd880e0fad00b1a811ebc43e39a00ab2"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2924b89b16420712e9bb8192396026a8fbd6d8726224f918353ac19c4c043d2a"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:16aa02e7a0f539098e215fc193c8926c897175d64c7926d00a36188917717a05"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:936a787f83db1f2115ee829dd615c4f684ee48ac4de5779ab4300994d8af325b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:459d6be6134ce3b38e0ef76f8a672924460c455d45f1ad8fdade36796df1ddc8"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f9ee4febb249c591d07b2d4dd36ebcad0ccd128962aaa1801508320896575ef"}, + {file = "pydantic_core-2.16.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:40a0bd0bed96dae5712dab2aba7d334a6c67cbcac2ddfca7dbcc4a8176445990"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:870dbfa94de9b8866b37b867a2cb37a60c401d9deb4a9ea392abf11a1f98037b"}, + {file = "pydantic_core-2.16.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:308974fdf98046db28440eb3377abba274808bf66262e042c412eb2adf852731"}, + {file = "pydantic_core-2.16.2-cp310-none-win32.whl", hash = "sha256:a477932664d9611d7a0816cc3c0eb1f8856f8a42435488280dfbf4395e141485"}, + {file = "pydantic_core-2.16.2-cp310-none-win_amd64.whl", hash = "sha256:8f9142a6ed83d90c94a3efd7af8873bf7cefed2d3d44387bf848888482e2d25f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:406fac1d09edc613020ce9cf3f2ccf1a1b2f57ab00552b4c18e3d5276c67eb11"}, + {file = "pydantic_core-2.16.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ce232a6170dd6532096cadbf6185271e4e8c70fc9217ebe105923ac105da9978"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a90fec23b4b05a09ad988e7a4f4e081711a90eb2a55b9c984d8b74597599180f"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8aafeedb6597a163a9c9727d8a8bd363a93277701b7bfd2749fbefee2396469e"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9957433c3a1b67bdd4c63717eaf174ebb749510d5ea612cd4e83f2d9142f3fc8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b0d7a9165167269758145756db43a133608a531b1e5bb6a626b9ee24bc38a8f7"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dffaf740fe2e147fedcb6b561353a16243e654f7fe8e701b1b9db148242e1272"}, + {file = "pydantic_core-2.16.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f8ed79883b4328b7f0bd142733d99c8e6b22703e908ec63d930b06be3a0e7113"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:cf903310a34e14651c9de056fcc12ce090560864d5a2bb0174b971685684e1d8"}, + {file = "pydantic_core-2.16.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:46b0d5520dbcafea9a8645a8164658777686c5c524d381d983317d29687cce97"}, + {file = "pydantic_core-2.16.2-cp311-none-win32.whl", hash = "sha256:70651ff6e663428cea902dac297066d5c6e5423fda345a4ca62430575364d62b"}, + {file = "pydantic_core-2.16.2-cp311-none-win_amd64.whl", hash = "sha256:98dc6f4f2095fc7ad277782a7c2c88296badcad92316b5a6e530930b1d475ebc"}, + {file = "pydantic_core-2.16.2-cp311-none-win_arm64.whl", hash = "sha256:ef6113cd31411eaf9b39fc5a8848e71c72656fd418882488598758b2c8c6dfa0"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:88646cae28eb1dd5cd1e09605680c2b043b64d7481cdad7f5003ebef401a3039"}, + {file = "pydantic_core-2.16.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7b883af50eaa6bb3299780651e5be921e88050ccf00e3e583b1e92020333304b"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bf26c2e2ea59d32807081ad51968133af3025c4ba5753e6a794683d2c91bf6e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:99af961d72ac731aae2a1b55ccbdae0733d816f8bfb97b41909e143de735f522"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:02906e7306cb8c5901a1feb61f9ab5e5c690dbbeaa04d84c1b9ae2a01ebe9379"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5362d099c244a2d2f9659fb3c9db7c735f0004765bbe06b99be69fbd87c3f15"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ac426704840877a285d03a445e162eb258924f014e2f074e209d9b4ff7bf380"}, + {file = "pydantic_core-2.16.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b94cbda27267423411c928208e89adddf2ea5dd5f74b9528513f0358bba019cb"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6db58c22ac6c81aeac33912fb1af0e930bc9774166cdd56eade913d5f2fff35e"}, + {file = "pydantic_core-2.16.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:396fdf88b1b503c9c59c84a08b6833ec0c3b5ad1a83230252a9e17b7dfb4cffc"}, + {file = "pydantic_core-2.16.2-cp312-none-win32.whl", hash = "sha256:7c31669e0c8cc68400ef0c730c3a1e11317ba76b892deeefaf52dcb41d56ed5d"}, + {file = "pydantic_core-2.16.2-cp312-none-win_amd64.whl", hash = "sha256:a3b7352b48fbc8b446b75f3069124e87f599d25afb8baa96a550256c031bb890"}, + {file = "pydantic_core-2.16.2-cp312-none-win_arm64.whl", hash = "sha256:a9e523474998fb33f7c1a4d55f5504c908d57add624599e095c20fa575b8d943"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:ae34418b6b389d601b31153b84dce480351a352e0bb763684a1b993d6be30f17"}, + {file = "pydantic_core-2.16.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:732bd062c9e5d9582a30e8751461c1917dd1ccbdd6cafb032f02c86b20d2e7ec"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4b52776a2e3230f4854907a1e0946eec04d41b1fc64069ee774876bbe0eab55"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ef551c053692b1e39e3f7950ce2296536728871110e7d75c4e7753fb30ca87f4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ebb892ed8599b23fa8f1799e13a12c87a97a6c9d0f497525ce9858564c4575a4"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa6c8c582036275997a733427b88031a32ffa5dfc3124dc25a730658c47a572f"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e4ba0884a91f1aecce75202473ab138724aa4fb26d7707f2e1fa6c3e68c84fbf"}, + {file = "pydantic_core-2.16.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7924e54f7ce5d253d6160090ddc6df25ed2feea25bfb3339b424a9dd591688bc"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:69a7b96b59322a81c2203be537957313b07dd333105b73db0b69212c7d867b4b"}, + {file = "pydantic_core-2.16.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:7e6231aa5bdacda78e96ad7b07d0c312f34ba35d717115f4b4bff6cb87224f0f"}, + {file = "pydantic_core-2.16.2-cp38-none-win32.whl", hash = "sha256:41dac3b9fce187a25c6253ec79a3f9e2a7e761eb08690e90415069ea4a68ff7a"}, + {file = "pydantic_core-2.16.2-cp38-none-win_amd64.whl", hash = "sha256:f685dbc1fdadb1dcd5b5e51e0a378d4685a891b2ddaf8e2bba89bd3a7144e44a"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:55749f745ebf154c0d63d46c8c58594d8894b161928aa41adbb0709c1fe78b77"}, + {file = "pydantic_core-2.16.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:b30b0dd58a4509c3bd7eefddf6338565c4905406aee0c6e4a5293841411a1286"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18de31781cdc7e7b28678df7c2d7882f9692ad060bc6ee3c94eb15a5d733f8f7"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5864b0242f74b9dd0b78fd39db1768bc3f00d1ffc14e596fd3e3f2ce43436a33"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b8f9186ca45aee030dc8234118b9c0784ad91a0bb27fc4e7d9d6608a5e3d386c"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc6f6c9be0ab6da37bc77c2dda5f14b1d532d5dbef00311ee6e13357a418e646"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:aa057095f621dad24a1e906747179a69780ef45cc8f69e97463692adbcdae878"}, + {file = "pydantic_core-2.16.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6ad84731a26bcfb299f9eab56c7932d46f9cad51c52768cace09e92a19e4cf55"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:3b052c753c4babf2d1edc034c97851f867c87d6f3ea63a12e2700f159f5c41c3"}, + {file = "pydantic_core-2.16.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:e0f686549e32ccdb02ae6f25eee40cc33900910085de6aa3790effd391ae10c2"}, + {file = "pydantic_core-2.16.2-cp39-none-win32.whl", hash = "sha256:7afb844041e707ac9ad9acad2188a90bffce2c770e6dc2318be0c9916aef1469"}, + {file = "pydantic_core-2.16.2-cp39-none-win_amd64.whl", hash = "sha256:9da90d393a8227d717c19f5397688a38635afec89f2e2d7af0df037f3249c39a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:5f60f920691a620b03082692c378661947d09415743e437a7478c309eb0e4f82"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:47924039e785a04d4a4fa49455e51b4eb3422d6eaacfde9fc9abf8fdef164e8a"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e6294e76b0380bb7a61eb8a39273c40b20beb35e8c87ee101062834ced19c545"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe56851c3f1d6f5384b3051c536cc81b3a93a73faf931f404fef95217cf1e10d"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9d776d30cde7e541b8180103c3f294ef7c1862fd45d81738d156d00551005784"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:72f7919af5de5ecfaf1eba47bf9a5d8aa089a3340277276e5636d16ee97614d7"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:4bfcbde6e06c56b30668a0c872d75a7ef3025dc3c1823a13cf29a0e9b33f67e8"}, + {file = "pydantic_core-2.16.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ff7c97eb7a29aba230389a2661edf2e9e06ce616c7e35aa764879b6894a44b25"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:9b5f13857da99325dcabe1cc4e9e6a3d7b2e2c726248ba5dd4be3e8e4a0b6d0e"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:a7e41e3ada4cca5f22b478c08e973c930e5e6c7ba3588fb8e35f2398cdcc1545"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:60eb8ceaa40a41540b9acae6ae7c1f0a67d233c40dc4359c256ad2ad85bdf5e5"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7beec26729d496a12fd23cf8da9944ee338c8b8a17035a560b585c36fe81af20"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:22c5f022799f3cd6741e24f0443ead92ef42be93ffda0d29b2597208c94c3753"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:eca58e319f4fd6df004762419612122b2c7e7d95ffafc37e890252f869f3fb2a"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:ed957db4c33bc99895f3a1672eca7e80e8cda8bd1e29a80536b4ec2153fa9804"}, + {file = "pydantic_core-2.16.2-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:459c0d338cc55d099798618f714b21b7ece17eb1a87879f2da20a3ff4c7628e2"}, + {file = "pydantic_core-2.16.2.tar.gz", hash = "sha256:0ba503850d8b8dcc18391f10de896ae51d37fe5fe43dbfb6a35c5c5cad271a06"}, +] + +[package.dependencies] +typing-extensions = ">=4.6.0,<4.7.0 || >4.7.0" + +[[package]] +name = "pytest" +version = "8.0.0" +description = "pytest: simple powerful testing with Python" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-8.0.0-py3-none-any.whl", hash = "sha256:50fb9cbe836c3f20f0dfa99c565201fb75dc54c8d76373cd1bde06b06657bdb6"}, + {file = "pytest-8.0.0.tar.gz", hash = "sha256:249b1b0864530ba251b7438274c4d251c58d868edaaec8762893ad4a0d71c36c"}, +] + +[package.dependencies] +colorama = {version = "*", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} +iniconfig = "*" +packaging = "*" +pluggy = ">=1.3.0,<2.0" +tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} + +[package.extras] +testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "python-dotenv" +version = "1.0.1" +description = "Read key-value pairs from a .env file and set them as environment variables" +optional = false +python-versions = ">=3.8" +files = [ + {file = "python-dotenv-1.0.1.tar.gz", hash = "sha256:e324ee90a023d808f1959c46bcbc04446a10ced277783dc6ee09987c37ec10ca"}, + {file = "python_dotenv-1.0.1-py3-none-any.whl", hash = "sha256:f7b63ef50f1b690dddf550d03497b66d609393b40b564ed0d674909a68ebf16a"}, +] + +[package.extras] +cli = ["click (>=5.0)"] + +[[package]] +name = "pyyaml" +version = "6.0.1" +description = "YAML parser and emitter for Python" +optional = false +python-versions = ">=3.6" +files = [ + {file = "PyYAML-6.0.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d858aa552c999bc8a8d57426ed01e40bef403cd8ccdd0fc5f6f04a00414cac2a"}, + {file = "PyYAML-6.0.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:fd66fc5d0da6d9815ba2cebeb4205f95818ff4b79c3ebe268e75d961704af52f"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:69b023b2b4daa7548bcfbd4aa3da05b3a74b772db9e23b982788168117739938"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:81e0b275a9ecc9c0c0c07b4b90ba548307583c125f54d5b6946cfee6360c733d"}, + {file = "PyYAML-6.0.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba336e390cd8e4d1739f42dfe9bb83a3cc2e80f567d8805e11b46f4a943f5515"}, + {file = "PyYAML-6.0.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:326c013efe8048858a6d312ddd31d56e468118ad4cdeda36c719bf5bb6192290"}, + {file = "PyYAML-6.0.1-cp310-cp310-win32.whl", hash = "sha256:bd4af7373a854424dabd882decdc5579653d7868b8fb26dc7d0e99f823aa5924"}, + {file = "PyYAML-6.0.1-cp310-cp310-win_amd64.whl", hash = "sha256:fd1592b3fdf65fff2ad0004b5e363300ef59ced41c2e6b3a99d4089fa8c5435d"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6965a7bc3cf88e5a1c3bd2e0b5c22f8d677dc88a455344035f03399034eb3007"}, + {file = "PyYAML-6.0.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:f003ed9ad21d6a4713f0a9b5a7a0a79e08dd0f221aff4525a2be4c346ee60aab"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:42f8152b8dbc4fe7d96729ec2b99c7097d656dc1213a3229ca5383f973a5ed6d"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:062582fca9fabdd2c8b54a3ef1c978d786e0f6b3a1510e0ac93ef59e0ddae2bc"}, + {file = "PyYAML-6.0.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d2b04aac4d386b172d5b9692e2d2da8de7bfb6c387fa4f801fbf6fb2e6ba4673"}, + {file = "PyYAML-6.0.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:e7d73685e87afe9f3b36c799222440d6cf362062f78be1013661b00c5c6f678b"}, + {file = "PyYAML-6.0.1-cp311-cp311-win32.whl", hash = "sha256:1635fd110e8d85d55237ab316b5b011de701ea0f29d07611174a1b42f1444741"}, + {file = "PyYAML-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:bf07ee2fef7014951eeb99f56f39c9bb4af143d8aa3c21b1677805985307da34"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:855fb52b0dc35af121542a76b9a84f8d1cd886ea97c84703eaa6d88e37a2ad28"}, + {file = "PyYAML-6.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40df9b996c2b73138957fe23a16a4f0ba614f4c0efce1e9406a184b6d07fa3a9"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a08c6f0fe150303c1c6b71ebcd7213c2858041a7e01975da3a99aed1e7a378ef"}, + {file = "PyYAML-6.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c22bec3fbe2524cde73d7ada88f6566758a8f7227bfbf93a408a9d86bcc12a0"}, + {file = "PyYAML-6.0.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8d4e9c88387b0f5c7d5f281e55304de64cf7f9c0021a3525bd3b1c542da3b0e4"}, + {file = "PyYAML-6.0.1-cp312-cp312-win32.whl", hash = "sha256:d483d2cdf104e7c9fa60c544d92981f12ad66a457afae824d146093b8c294c54"}, + {file = "PyYAML-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:0d3304d8c0adc42be59c5f8a4d9e3d7379e6955ad754aa9d6ab7a398b59dd1df"}, + {file = "PyYAML-6.0.1-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:50550eb667afee136e9a77d6dc71ae76a44df8b3e51e41b77f6de2932bfe0f47"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1fe35611261b29bd1de0070f0b2f47cb6ff71fa6595c077e42bd0c419fa27b98"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:704219a11b772aea0d8ecd7058d0082713c3562b4e271b849ad7dc4a5c90c13c"}, + {file = "PyYAML-6.0.1-cp36-cp36m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:afd7e57eddb1a54f0f1a974bc4391af8bcce0b444685d936840f125cf046d5bd"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win32.whl", hash = "sha256:fca0e3a251908a499833aa292323f32437106001d436eca0e6e7833256674585"}, + {file = "PyYAML-6.0.1-cp36-cp36m-win_amd64.whl", hash = "sha256:f22ac1c3cac4dbc50079e965eba2c1058622631e526bd9afd45fedd49ba781fa"}, + {file = "PyYAML-6.0.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:b1275ad35a5d18c62a7220633c913e1b42d44b46ee12554e5fd39c70a243d6a3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:18aeb1bf9a78867dc38b259769503436b7c72f7a1f1f4c93ff9a17de54319b27"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:596106435fa6ad000c2991a98fa58eeb8656ef2325d7e158344fb33864ed87e3"}, + {file = "PyYAML-6.0.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:baa90d3f661d43131ca170712d903e6295d1f7a0f595074f151c0aed377c9b9c"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win32.whl", hash = "sha256:9046c58c4395dff28dd494285c82ba00b546adfc7ef001486fbf0324bc174fba"}, + {file = "PyYAML-6.0.1-cp37-cp37m-win_amd64.whl", hash = "sha256:4fb147e7a67ef577a588a0e2c17b6db51dda102c71de36f8549b6816a96e1867"}, + {file = "PyYAML-6.0.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:1d4c7e777c441b20e32f52bd377e0c409713e8bb1386e1099c2415f26e479595"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a0cd17c15d3bb3fa06978b4e8958dcdc6e0174ccea823003a106c7d4d7899ac5"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28c119d996beec18c05208a8bd78cbe4007878c6dd15091efb73a30e90539696"}, + {file = "PyYAML-6.0.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7e07cbde391ba96ab58e532ff4803f79c4129397514e1413a7dc761ccd755735"}, + {file = "PyYAML-6.0.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:49a183be227561de579b4a36efbb21b3eab9651dd81b1858589f796549873dd6"}, + {file = "PyYAML-6.0.1-cp38-cp38-win32.whl", hash = "sha256:184c5108a2aca3c5b3d3bf9395d50893a7ab82a38004c8f61c258d4428e80206"}, + {file = "PyYAML-6.0.1-cp38-cp38-win_amd64.whl", hash = "sha256:1e2722cc9fbb45d9b87631ac70924c11d3a401b2d7f410cc0e3bbf249f2dca62"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9eb6caa9a297fc2c2fb8862bc5370d0303ddba53ba97e71f08023b6cd73d16a8"}, + {file = "PyYAML-6.0.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c8098ddcc2a85b61647b2590f825f3db38891662cfc2fc776415143f599bb859"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5773183b6446b2c99bb77e77595dd486303b4faab2b086e7b17bc6bef28865f6"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b786eecbdf8499b9ca1d697215862083bd6d2a99965554781d0d8d1ad31e13a0"}, + {file = "PyYAML-6.0.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc1bf2925a1ecd43da378f4db9e4f799775d6367bdb94671027b73b393a7c42c"}, + {file = "PyYAML-6.0.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:04ac92ad1925b2cff1db0cfebffb6ffc43457495c9b3c39d3fcae417d7125dc5"}, + {file = "PyYAML-6.0.1-cp39-cp39-win32.whl", hash = "sha256:faca3bdcf85b2fc05d06ff3fbc1f83e1391b3e724afa3feba7d13eeab355484c"}, + {file = "PyYAML-6.0.1-cp39-cp39-win_amd64.whl", hash = "sha256:510c9deebc5c0225e8c96813043e62b680ba2f9c50a08d3724c7f28a747d1486"}, + {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, +] + +[[package]] +name = "sniffio" +version = "1.3.0" +description = "Sniff out which async library your code is running under" +optional = false +python-versions = ">=3.7" +files = [ + {file = "sniffio-1.3.0-py3-none-any.whl", hash = "sha256:eecefdce1e5bbfb7ad2eeaabf7c1eeb404d7757c379bd1f7e5cce9d8bf425384"}, + {file = "sniffio-1.3.0.tar.gz", hash = "sha256:e60305c5e5d314f5389259b7f22aaa33d8f7dee49763119234af3755c55b9101"}, +] + +[[package]] +name = "starlette" +version = "0.36.3" +description = "The little ASGI library that shines." +optional = false +python-versions = ">=3.8" +files = [ + {file = "starlette-0.36.3-py3-none-any.whl", hash = "sha256:13d429aa93a61dc40bf503e8c801db1f1bca3dc706b10ef2434a36123568f044"}, + {file = "starlette-0.36.3.tar.gz", hash = "sha256:90a671733cfb35771d8cc605e0b679d23b992f8dcfad48cc60b38cb29aeb7080"}, +] + +[package.dependencies] +anyio = ">=3.4.0,<5" + +[package.extras] +full = ["httpx (>=0.22.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.7)", "pyyaml"] + +[[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.9.0" +description = "Backported and Experimental Type Hints for Python 3.8+" +optional = false +python-versions = ">=3.8" +files = [ + {file = "typing_extensions-4.9.0-py3-none-any.whl", hash = "sha256:af72aea155e91adfc61c3ae9e0e342dbc0cba726d6cba4b6c72c1f34e47291cd"}, + {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, +] + +[[package]] +name = "uvicorn" +version = "0.27.1" +description = "The lightning-fast ASGI server." +optional = false +python-versions = ">=3.8" +files = [ + {file = "uvicorn-0.27.1-py3-none-any.whl", hash = "sha256:5c89da2f3895767472a35556e539fd59f7edbe9b1e9c0e1c99eebeadc61838e4"}, + {file = "uvicorn-0.27.1.tar.gz", hash = "sha256:3d9a267296243532db80c83a959a3400502165ade2c1338dea4e67915fd4745a"}, +] + +[package.dependencies] +click = ">=7.0" +colorama = {version = ">=0.4", optional = true, markers = "sys_platform == \"win32\" and extra == \"standard\""} +h11 = ">=0.8" +httptools = {version = ">=0.5.0", optional = true, markers = "extra == \"standard\""} +python-dotenv = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +pyyaml = {version = ">=5.1", optional = true, markers = "extra == \"standard\""} +typing-extensions = {version = ">=4.0", markers = "python_version < \"3.11\""} +uvloop = {version = ">=0.14.0,<0.15.0 || >0.15.0,<0.15.1 || >0.15.1", optional = true, markers = "(sys_platform != \"win32\" and sys_platform != \"cygwin\") and platform_python_implementation != \"PyPy\" and extra == \"standard\""} +watchfiles = {version = ">=0.13", optional = true, markers = "extra == \"standard\""} +websockets = {version = ">=10.4", optional = true, markers = "extra == \"standard\""} + +[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 = "uvloop" +version = "0.19.0" +description = "Fast implementation of asyncio event loop on top of libuv" +optional = false +python-versions = ">=3.8.0" +files = [ + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:de4313d7f575474c8f5a12e163f6d89c0a878bc49219641d49e6f1444369a90e"}, + {file = "uvloop-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:5588bd21cf1fcf06bded085f37e43ce0e00424197e7c10e77afd4bbefffef428"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b1fd71c3843327f3bbc3237bedcdb6504fd50368ab3e04d0410e52ec293f5b8"}, + {file = "uvloop-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a05128d315e2912791de6088c34136bfcdd0c7cbc1cf85fd6fd1bb321b7c849"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:cd81bdc2b8219cb4b2556eea39d2e36bfa375a2dd021404f90a62e44efaaf957"}, + {file = "uvloop-0.19.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:5f17766fb6da94135526273080f3455a112f82570b2ee5daa64d682387fe0dcd"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ce6b0af8f2729a02a5d1575feacb2a94fc7b2e983868b009d51c9a9d2149bef"}, + {file = "uvloop-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:31e672bb38b45abc4f26e273be83b72a0d28d074d5b370fc4dcf4c4eb15417d2"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:570fc0ed613883d8d30ee40397b79207eedd2624891692471808a95069a007c1"}, + {file = "uvloop-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5138821e40b0c3e6c9478643b4660bd44372ae1e16a322b8fc07478f92684e24"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:91ab01c6cd00e39cde50173ba4ec68a1e578fee9279ba64f5221810a9e786533"}, + {file = "uvloop-0.19.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:47bf3e9312f63684efe283f7342afb414eea4d3011542155c7e625cd799c3b12"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:da8435a3bd498419ee8c13c34b89b5005130a476bda1d6ca8cfdde3de35cd650"}, + {file = "uvloop-0.19.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:02506dc23a5d90e04d4f65c7791e65cf44bd91b37f24cfc3ef6cf2aff05dc7ec"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2693049be9d36fef81741fddb3f441673ba12a34a704e7b4361efb75cf30befc"}, + {file = "uvloop-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7010271303961c6f0fe37731004335401eb9075a12680738731e9c92ddd96ad6"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:5daa304d2161d2918fa9a17d5635099a2f78ae5b5960e742b2fcfbb7aefaa593"}, + {file = "uvloop-0.19.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:7207272c9520203fea9b93843bb775d03e1cf88a80a936ce760f60bb5add92f3"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:78ab247f0b5671cc887c31d33f9b3abfb88d2614b84e4303f1a63b46c046c8bd"}, + {file = "uvloop-0.19.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:472d61143059c84947aa8bb74eabbace30d577a03a1805b77933d6bd13ddebbd"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:45bf4c24c19fb8a50902ae37c5de50da81de4922af65baf760f7c0c42e1088be"}, + {file = "uvloop-0.19.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271718e26b3e17906b28b67314c45d19106112067205119dddbd834c2b7ce797"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:34175c9fd2a4bc3adc1380e1261f60306344e3407c20a4d684fd5f3be010fa3d"}, + {file = "uvloop-0.19.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:e27f100e1ff17f6feeb1f33968bc185bf8ce41ca557deee9d9bbbffeb72030b7"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:13dfdf492af0aa0a0edf66807d2b465607d11c4fa48f4a1fd41cbea5b18e8e8b"}, + {file = "uvloop-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:6e3d4e85ac060e2342ff85e90d0c04157acb210b9ce508e784a944f852a40e67"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8ca4956c9ab567d87d59d49fa3704cf29e37109ad348f2d5223c9bf761a332e7"}, + {file = "uvloop-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f467a5fd23b4fc43ed86342641f3936a68ded707f4627622fa3f82a120e18256"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:492e2c32c2af3f971473bc22f086513cedfc66a130756145a931a90c3958cb17"}, + {file = "uvloop-0.19.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2df95fca285a9f5bfe730e51945ffe2fa71ccbfdde3b0da5772b4ee4f2e770d5"}, + {file = "uvloop-0.19.0.tar.gz", hash = "sha256:0246f4fd1bf2bf702e06b0d45ee91677ee5c31242f39aab4ea6fe0c51aedd0fd"}, +] + +[package.extras] +docs = ["Sphinx (>=4.1.2,<4.2.0)", "sphinx-rtd-theme (>=0.5.2,<0.6.0)", "sphinxcontrib-asyncio (>=0.3.0,<0.4.0)"] +test = ["Cython (>=0.29.36,<0.30.0)", "aiohttp (==3.9.0b0)", "aiohttp (>=3.8.1)", "flake8 (>=5.0,<6.0)", "mypy (>=0.800)", "psutil", "pyOpenSSL (>=23.0.0,<23.1.0)", "pycodestyle (>=2.9.0,<2.10.0)"] + +[[package]] +name = "watchfiles" +version = "0.21.0" +description = "Simple, modern and high performance file watching and code reload in python." +optional = false +python-versions = ">=3.8" +files = [ + {file = "watchfiles-0.21.0-cp310-cp310-macosx_10_7_x86_64.whl", hash = "sha256:27b4035013f1ea49c6c0b42d983133b136637a527e48c132d368eb19bf1ac6aa"}, + {file = "watchfiles-0.21.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c81818595eff6e92535ff32825f31c116f867f64ff8cdf6562cd1d6b2e1e8f3e"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:6c107ea3cf2bd07199d66f156e3ea756d1b84dfd43b542b2d870b77868c98c03"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0d9ac347653ebd95839a7c607608703b20bc07e577e870d824fa4801bc1cb124"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5eb86c6acb498208e7663ca22dbe68ca2cf42ab5bf1c776670a50919a56e64ab"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f564bf68404144ea6b87a78a3f910cc8de216c6b12a4cf0b27718bf4ec38d303"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3d0f32ebfaa9c6011f8454994f86108c2eb9c79b8b7de00b36d558cadcedaa3d"}, + {file = "watchfiles-0.21.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d45d9b699ecbac6c7bd8e0a2609767491540403610962968d258fd6405c17c"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:aff06b2cac3ef4616e26ba17a9c250c1fe9dd8a5d907d0193f84c499b1b6e6a9"}, + {file = "watchfiles-0.21.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:d9792dff410f266051025ecfaa927078b94cc7478954b06796a9756ccc7e14a9"}, + {file = "watchfiles-0.21.0-cp310-none-win32.whl", hash = "sha256:214cee7f9e09150d4fb42e24919a1e74d8c9b8a9306ed1474ecaddcd5479c293"}, + {file = "watchfiles-0.21.0-cp310-none-win_amd64.whl", hash = "sha256:1ad7247d79f9f55bb25ab1778fd47f32d70cf36053941f07de0b7c4e96b5d235"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_10_7_x86_64.whl", hash = "sha256:668c265d90de8ae914f860d3eeb164534ba2e836811f91fecc7050416ee70aa7"}, + {file = "watchfiles-0.21.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3a23092a992e61c3a6a70f350a56db7197242f3490da9c87b500f389b2d01eef"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:e7941bbcfdded9c26b0bf720cb7e6fd803d95a55d2c14b4bd1f6a2772230c586"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11cd0c3100e2233e9c53106265da31d574355c288e15259c0d40a4405cbae317"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d78f30cbe8b2ce770160d3c08cff01b2ae9306fe66ce899b73f0409dc1846c1b"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6674b00b9756b0af620aa2a3346b01f8e2a3dc729d25617e1b89cf6af4a54eb1"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:fd7ac678b92b29ba630d8c842d8ad6c555abda1b9ef044d6cc092dacbfc9719d"}, + {file = "watchfiles-0.21.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c873345680c1b87f1e09e0eaf8cf6c891b9851d8b4d3645e7efe2ec20a20cc7"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:49f56e6ecc2503e7dbe233fa328b2be1a7797d31548e7a193237dcdf1ad0eee0"}, + {file = "watchfiles-0.21.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:02d91cbac553a3ad141db016e3350b03184deaafeba09b9d6439826ee594b365"}, + {file = "watchfiles-0.21.0-cp311-none-win32.whl", hash = "sha256:ebe684d7d26239e23d102a2bad2a358dedf18e462e8808778703427d1f584400"}, + {file = "watchfiles-0.21.0-cp311-none-win_amd64.whl", hash = "sha256:4566006aa44cb0d21b8ab53baf4b9c667a0ed23efe4aaad8c227bfba0bf15cbe"}, + {file = "watchfiles-0.21.0-cp311-none-win_arm64.whl", hash = "sha256:c550a56bf209a3d987d5a975cdf2063b3389a5d16caf29db4bdddeae49f22078"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_10_7_x86_64.whl", hash = "sha256:51ddac60b96a42c15d24fbdc7a4bfcd02b5a29c047b7f8bf63d3f6f5a860949a"}, + {file = "watchfiles-0.21.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:511f0b034120cd1989932bf1e9081aa9fb00f1f949fbd2d9cab6264916ae89b1"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:cfb92d49dbb95ec7a07511bc9efb0faff8fe24ef3805662b8d6808ba8409a71a"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3f92944efc564867bbf841c823c8b71bb0be75e06b8ce45c084b46411475a915"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:642d66b75eda909fd1112d35c53816d59789a4b38c141a96d62f50a3ef9b3360"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d23bcd6c8eaa6324fe109d8cac01b41fe9a54b8c498af9ce464c1aeeb99903d6"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18d5b4da8cf3e41895b34e8c37d13c9ed294954907929aacd95153508d5d89d7"}, + {file = "watchfiles-0.21.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1b8d1eae0f65441963d805f766c7e9cd092f91e0c600c820c764a4ff71a0764c"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:1fd9a5205139f3c6bb60d11f6072e0552f0a20b712c85f43d42342d162be1235"}, + {file = "watchfiles-0.21.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a1e3014a625bcf107fbf38eece0e47fa0190e52e45dc6eee5a8265ddc6dc5ea7"}, + {file = "watchfiles-0.21.0-cp312-none-win32.whl", hash = "sha256:9d09869f2c5a6f2d9df50ce3064b3391d3ecb6dced708ad64467b9e4f2c9bef3"}, + {file = "watchfiles-0.21.0-cp312-none-win_amd64.whl", hash = "sha256:18722b50783b5e30a18a8a5db3006bab146d2b705c92eb9a94f78c72beb94094"}, + {file = "watchfiles-0.21.0-cp312-none-win_arm64.whl", hash = "sha256:a3b9bec9579a15fb3ca2d9878deae789df72f2b0fdaf90ad49ee389cad5edab6"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_10_7_x86_64.whl", hash = "sha256:4ea10a29aa5de67de02256a28d1bf53d21322295cb00bd2d57fcd19b850ebd99"}, + {file = "watchfiles-0.21.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:40bca549fdc929b470dd1dbfcb47b3295cb46a6d2c90e50588b0a1b3bd98f429"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:9b37a7ba223b2f26122c148bb8d09a9ff312afca998c48c725ff5a0a632145f7"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ec8c8900dc5c83650a63dd48c4d1d245343f904c4b64b48798c67a3767d7e165"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8ad3fe0a3567c2f0f629d800409cd528cb6251da12e81a1f765e5c5345fd0137"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d353c4cfda586db2a176ce42c88f2fc31ec25e50212650c89fdd0f560ee507b"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:83a696da8922314ff2aec02987eefb03784f473281d740bf9170181829133765"}, + {file = "watchfiles-0.21.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5a03651352fc20975ee2a707cd2d74a386cd303cc688f407296064ad1e6d1562"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:3ad692bc7792be8c32918c699638b660c0de078a6cbe464c46e1340dadb94c19"}, + {file = "watchfiles-0.21.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06247538e8253975bdb328e7683f8515ff5ff041f43be6c40bff62d989b7d0b0"}, + {file = "watchfiles-0.21.0-cp38-none-win32.whl", hash = "sha256:9a0aa47f94ea9a0b39dd30850b0adf2e1cd32a8b4f9c7aa443d852aacf9ca214"}, + {file = "watchfiles-0.21.0-cp38-none-win_amd64.whl", hash = "sha256:8d5f400326840934e3507701f9f7269247f7c026d1b6cfd49477d2be0933cfca"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_10_7_x86_64.whl", hash = "sha256:7f762a1a85a12cc3484f77eee7be87b10f8c50b0b787bb02f4e357403cad0c0e"}, + {file = "watchfiles-0.21.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:6e9be3ef84e2bb9710f3f777accce25556f4a71e15d2b73223788d528fcc2052"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:4c48a10d17571d1275701e14a601e36959ffada3add8cdbc9e5061a6e3579a5d"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6c889025f59884423428c261f212e04d438de865beda0b1e1babab85ef4c0f01"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:66fac0c238ab9a2e72d026b5fb91cb902c146202bbd29a9a1a44e8db7b710b6f"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b4a21f71885aa2744719459951819e7bf5a906a6448a6b2bbce8e9cc9f2c8128"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c9198c989f47898b2c22201756f73249de3748e0fc9de44adaf54a8b259cc0c"}, + {file = "watchfiles-0.21.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d8f57c4461cd24fda22493109c45b3980863c58a25b8bec885ca8bea6b8d4b28"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:853853cbf7bf9408b404754b92512ebe3e3a83587503d766d23e6bf83d092ee6"}, + {file = "watchfiles-0.21.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d5b1dc0e708fad9f92c296ab2f948af403bf201db8fb2eb4c8179db143732e49"}, + {file = "watchfiles-0.21.0-cp39-none-win32.whl", hash = "sha256:59137c0c6826bd56c710d1d2bda81553b5e6b7c84d5a676747d80caf0409ad94"}, + {file = "watchfiles-0.21.0-cp39-none-win_amd64.whl", hash = "sha256:6cb8fdc044909e2078c248986f2fc76f911f72b51ea4a4fbbf472e01d14faa58"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_10_7_x86_64.whl", hash = "sha256:ab03a90b305d2588e8352168e8c5a1520b721d2d367f31e9332c4235b30b8994"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:927c589500f9f41e370b0125c12ac9e7d3a2fd166b89e9ee2828b3dda20bfe6f"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1bd467213195e76f838caf2c28cd65e58302d0254e636e7c0fca81efa4a2e62c"}, + {file = "watchfiles-0.21.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:02b73130687bc3f6bb79d8a170959042eb56eb3a42df3671c79b428cd73f17cc"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_10_7_x86_64.whl", hash = "sha256:08dca260e85ffae975448e344834d765983237ad6dc308231aa16e7933db763e"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-macosx_11_0_arm64.whl", hash = "sha256:3ccceb50c611c433145502735e0370877cced72a6c70fd2410238bcbc7fe51d8"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:57d430f5fb63fea141ab71ca9c064e80de3a20b427ca2febcbfcef70ff0ce895"}, + {file = "watchfiles-0.21.0-pp38-pypy38_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0dd5fad9b9c0dd89904bbdea978ce89a2b692a7ee8a0ce19b940e538c88a809c"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_10_7_x86_64.whl", hash = "sha256:be6dd5d52b73018b21adc1c5d28ac0c68184a64769052dfeb0c5d9998e7f56a2"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:b3cab0e06143768499384a8a5efb9c4dc53e19382952859e4802f294214f36ec"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c6ed10c2497e5fedadf61e465b3ca12a19f96004c15dcffe4bd442ebadc2d85"}, + {file = "watchfiles-0.21.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:43babacef21c519bc6631c5fce2a61eccdfc011b4bcb9047255e9620732c8097"}, + {file = "watchfiles-0.21.0.tar.gz", hash = "sha256:c76c635fabf542bb78524905718c39f736a98e5ab25b23ec6d4abede1a85a6a3"}, +] + +[package.dependencies] +anyio = ">=3.0.0" + +[[package]] +name = "websockets" +version = "12.0" +description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" +optional = false +python-versions = ">=3.8" +files = [ + {file = "websockets-12.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d554236b2a2006e0ce16315c16eaa0d628dab009c33b63ea03f41c6107958374"}, + {file = "websockets-12.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2d225bb6886591b1746b17c0573e29804619c8f755b5598d875bb4235ea639be"}, + {file = "websockets-12.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eb809e816916a3b210bed3c82fb88eaf16e8afcf9c115ebb2bacede1797d2547"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c588f6abc13f78a67044c6b1273a99e1cf31038ad51815b3b016ce699f0d75c2"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5aa9348186d79a5f232115ed3fa9020eab66d6c3437d72f9d2c8ac0c6858c558"}, + {file = "websockets-12.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6350b14a40c95ddd53e775dbdbbbc59b124a5c8ecd6fbb09c2e52029f7a9f480"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:70ec754cc2a769bcd218ed8d7209055667b30860ffecb8633a834dde27d6307c"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:6e96f5ed1b83a8ddb07909b45bd94833b0710f738115751cdaa9da1fb0cb66e8"}, + {file = "websockets-12.0-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:4d87be612cbef86f994178d5186add3d94e9f31cc3cb499a0482b866ec477603"}, + {file = "websockets-12.0-cp310-cp310-win32.whl", hash = "sha256:befe90632d66caaf72e8b2ed4d7f02b348913813c8b0a32fae1cc5fe3730902f"}, + {file = "websockets-12.0-cp310-cp310-win_amd64.whl", hash = "sha256:363f57ca8bc8576195d0540c648aa58ac18cf85b76ad5202b9f976918f4219cf"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:5d873c7de42dea355d73f170be0f23788cf3fa9f7bed718fd2830eefedce01b4"}, + {file = "websockets-12.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3f61726cae9f65b872502ff3c1496abc93ffbe31b278455c418492016e2afc8f"}, + {file = "websockets-12.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed2fcf7a07334c77fc8a230755c2209223a7cc44fc27597729b8ef5425aa61a3"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e332c210b14b57904869ca9f9bf4ca32f5427a03eeb625da9b616c85a3a506c"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5693ef74233122f8ebab026817b1b37fe25c411ecfca084b29bc7d6efc548f45"}, + {file = "websockets-12.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e9e7db18b4539a29cc5ad8c8b252738a30e2b13f033c2d6e9d0549b45841c04"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:6e2df67b8014767d0f785baa98393725739287684b9f8d8a1001eb2839031447"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:bea88d71630c5900690fcb03161ab18f8f244805c59e2e0dc4ffadae0a7ee0ca"}, + {file = "websockets-12.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:dff6cdf35e31d1315790149fee351f9e52978130cef6c87c4b6c9b3baf78bc53"}, + {file = "websockets-12.0-cp311-cp311-win32.whl", hash = "sha256:3e3aa8c468af01d70332a382350ee95f6986db479ce7af14d5e81ec52aa2b402"}, + {file = "websockets-12.0-cp311-cp311-win_amd64.whl", hash = "sha256:25eb766c8ad27da0f79420b2af4b85d29914ba0edf69f547cc4f06ca6f1d403b"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0e6e2711d5a8e6e482cacb927a49a3d432345dfe7dea8ace7b5790df5932e4df"}, + {file = "websockets-12.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:dbcf72a37f0b3316e993e13ecf32f10c0e1259c28ffd0a85cee26e8549595fbc"}, + {file = "websockets-12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:12743ab88ab2af1d17dd4acb4645677cb7063ef4db93abffbf164218a5d54c6b"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b645f491f3c48d3f8a00d1fce07445fab7347fec54a3e65f0725d730d5b99cb"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9893d1aa45a7f8b3bc4510f6ccf8db8c3b62120917af15e3de247f0780294b92"}, + {file = "websockets-12.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f38a7b376117ef7aff996e737583172bdf535932c9ca021746573bce40165ed"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:f764ba54e33daf20e167915edc443b6f88956f37fb606449b4a5b10ba42235a5"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:1e4b3f8ea6a9cfa8be8484c9221ec0257508e3a1ec43c36acdefb2a9c3b00aa2"}, + {file = "websockets-12.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:9fdf06fd06c32205a07e47328ab49c40fc1407cdec801d698a7c41167ea45113"}, + {file = "websockets-12.0-cp312-cp312-win32.whl", hash = "sha256:baa386875b70cbd81798fa9f71be689c1bf484f65fd6fb08d051a0ee4e79924d"}, + {file = "websockets-12.0-cp312-cp312-win_amd64.whl", hash = "sha256:ae0a5da8f35a5be197f328d4727dbcfafa53d1824fac3d96cdd3a642fe09394f"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:5f6ffe2c6598f7f7207eef9a1228b6f5c818f9f4d53ee920aacd35cec8110438"}, + {file = "websockets-12.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:9edf3fc590cc2ec20dc9d7a45108b5bbaf21c0d89f9fd3fd1685e223771dc0b2"}, + {file = "websockets-12.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:8572132c7be52632201a35f5e08348137f658e5ffd21f51f94572ca6c05ea81d"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:604428d1b87edbf02b233e2c207d7d528460fa978f9e391bd8aaf9c8311de137"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1a9d160fd080c6285e202327aba140fc9a0d910b09e423afff4ae5cbbf1c7205"}, + {file = "websockets-12.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:87b4aafed34653e465eb77b7c93ef058516cb5acf3eb21e42f33928616172def"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:b2ee7288b85959797970114deae81ab41b731f19ebcd3bd499ae9ca0e3f1d2c8"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:7fa3d25e81bfe6a89718e9791128398a50dec6d57faf23770787ff441d851967"}, + {file = "websockets-12.0-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:a571f035a47212288e3b3519944f6bf4ac7bc7553243e41eac50dd48552b6df7"}, + {file = "websockets-12.0-cp38-cp38-win32.whl", hash = "sha256:3c6cc1360c10c17463aadd29dd3af332d4a1adaa8796f6b0e9f9df1fdb0bad62"}, + {file = "websockets-12.0-cp38-cp38-win_amd64.whl", hash = "sha256:1bf386089178ea69d720f8db6199a0504a406209a0fc23e603b27b300fdd6892"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:ab3d732ad50a4fbd04a4490ef08acd0517b6ae6b77eb967251f4c263011a990d"}, + {file = "websockets-12.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:a1d9697f3337a89691e3bd8dc56dea45a6f6d975f92e7d5f773bc715c15dde28"}, + {file = "websockets-12.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:1df2fbd2c8a98d38a66f5238484405b8d1d16f929bb7a33ed73e4801222a6f53"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:23509452b3bc38e3a057382c2e941d5ac2e01e251acce7adc74011d7d8de434c"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2e5fc14ec6ea568200ea4ef46545073da81900a2b67b3e666f04adf53ad452ec"}, + {file = "websockets-12.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:46e71dbbd12850224243f5d2aeec90f0aaa0f2dde5aeeb8fc8df21e04d99eff9"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:b81f90dcc6c85a9b7f29873beb56c94c85d6f0dac2ea8b60d995bd18bf3e2aae"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:a02413bc474feda2849c59ed2dfb2cddb4cd3d2f03a2fedec51d6e959d9b608b"}, + {file = "websockets-12.0-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:bbe6013f9f791944ed31ca08b077e26249309639313fff132bfbf3ba105673b9"}, + {file = "websockets-12.0-cp39-cp39-win32.whl", hash = "sha256:cbe83a6bbdf207ff0541de01e11904827540aa069293696dd528a6640bd6a5f6"}, + {file = "websockets-12.0-cp39-cp39-win_amd64.whl", hash = "sha256:fc4e7fa5414512b481a2483775a8e8be7803a35b30ca805afa4998a84f9fd9e8"}, + {file = "websockets-12.0-pp310-pypy310_pp73-macosx_10_9_x86_64.whl", hash = "sha256:248d8e2446e13c1d4326e0a6a4e9629cb13a11195051a73acf414812700badbd"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f44069528d45a933997a6fef143030d8ca8042f0dfaad753e2906398290e2870"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c4e37d36f0d19f0a4413d3e18c0d03d0c268ada2061868c1e6f5ab1a6d575077"}, + {file = "websockets-12.0-pp310-pypy310_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3d829f975fc2e527a3ef2f9c8f25e553eb7bc779c6665e8e1d52aa22800bb38b"}, + {file = "websockets-12.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:2c71bd45a777433dd9113847af751aae36e448bc6b8c361a566cb043eda6ec30"}, + {file = "websockets-12.0-pp38-pypy38_pp73-macosx_10_9_x86_64.whl", hash = "sha256:0bee75f400895aef54157b36ed6d3b308fcab62e5260703add87f44cee9c82a6"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:423fc1ed29f7512fceb727e2d2aecb952c46aa34895e9ed96071821309951123"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:27a5e9964ef509016759f2ef3f2c1e13f403725a5e6a1775555994966a66e931"}, + {file = "websockets-12.0-pp38-pypy38_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3181df4583c4d3994d31fb235dc681d2aaad744fbdbf94c4802485ececdecf2"}, + {file = "websockets-12.0-pp38-pypy38_pp73-win_amd64.whl", hash = "sha256:b067cb952ce8bf40115f6c19f478dc71c5e719b7fbaa511359795dfd9d1a6468"}, + {file = "websockets-12.0-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:00700340c6c7ab788f176d118775202aadea7602c5cc6be6ae127761c16d6b0b"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e469d01137942849cff40517c97a30a93ae79917752b34029f0ec72df6b46399"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffefa1374cd508d633646d51a8e9277763a9b78ae71324183693959cf94635a7"}, + {file = "websockets-12.0-pp39-pypy39_pp73-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba0cab91b3956dfa9f512147860783a1829a8d905ee218a9837c18f683239611"}, + {file = "websockets-12.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:2cb388a5bfb56df4d9a406783b7f9dbefb888c09b71629351cc6b036e9259370"}, + {file = "websockets-12.0-py3-none-any.whl", hash = "sha256:dc284bbc8d7c78a6c69e0c7325ab46ee5e40bb4d50e494d8131a07ef47500e9e"}, + {file = "websockets-12.0.tar.gz", hash = "sha256:81df9cbcbb6c260de1e007e58c011bfebe2dafc8435107b0537f393dd38c8b1b"}, +] + +[metadata] +lock-version = "2.0" +python-versions = "^3.10" +content-hash = "589139ad1ef5392abb74dd4b1f496259e5f2766795efa1cb2599c6fe3fed3cd9" diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml new file mode 100644 index 000000000..411387e77 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml @@ -0,0 +1,19 @@ +[tool.poetry] +name = "electricity-rate-simulator" +version = "0.1.0" +description = "" +authors = ["Your Name "] +readme = "README.md" + +[tool.poetry.dependencies] +python = "^3.10" +fastapi = "^0.109.2" +uvicorn = {extras = ["standard"], version = "^0.27.1"} + + +[tool.poetry.group.dev.dependencies] +pytest = "^8.0.0" + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/__init__.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/__init__.py new file mode 100644 index 000000000..e69de29bb From 9150ffcd5548bf95e111af000cdf17c8a7246ff9 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Wed, 21 Feb 2024 20:35:37 +0000 Subject: [PATCH 03/21] add electric simurate of core for prototype. --- .../core/__init__.py | 0 .../core/electric_simurate.py | 171 ++++++++++++++++++ .../data/plovider/jxtg-electric/plan.csv | 5 + .../data/plovider/looop/plan.csv | 8 + .../data/plovider/tepco/plan.csv | 8 + .../data/plovider/tokyo-gas/plan.csv | 5 + .../tests/test_core.py | 38 ++++ 7 files changed, 235 insertions(+) create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/__init__.py create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/jxtg-electric/plan.csv create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/looop/plan.csv create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tepco/plan.csv create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tokyo-gas/plan.csv create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/__init__.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py new file mode 100644 index 000000000..635dbb53b --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py @@ -0,0 +1,171 @@ +from pathlib import Path +import csv + +BASE_DIR = Path( + "/workspaces/coding-challenge/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator" +) +DATA_DIR = BASE_DIR.joinpath("data") +TEPCO_DIR = DATA_DIR.joinpath("plovider", "tepco", "plan.csv") +LOOOP_DIR = DATA_DIR.joinpath("plovider", "looop", "plan.csv") +JXTG_DIR = DATA_DIR.joinpath("plovider", "jxtg-electric", "plan.csv") +TOKYOGAS_DIR = DATA_DIR.joinpath("plovider", "tokyo-gas", "plan.csv") + + +def calc_usage_rate_by_tepco(amount: int): + """電気料金計算 + + e.g.) + 最初の120kWhまで(第1段階料金) 1kWh 30円00銭 + 120kWhをこえ300kWhまで(第2段階料金) 〃 36円60銭 + 上記超過(第3段階料金) 〃 40円69銭 + + Args: + amount (_type_): _description_ + + """ + if not amount: + raise ValueError(f"AmountValueError: {amount}") + + if amount <= 120: + return amount * 30.00 + elif amount <= 300: + over_amount = amount - 120 + return 120 * 30.00 + over_amount * 36.60 + else: + over_amount = amount - 300 + return 120 * 30.00 + 180 * 36.60 + over_amount * 40.69 + + +def calc_usage_rate_by_looop(amount: int): + if not amount: + raise ValueError(f"AmountValueError: {amount}") + + return amount * 26.40 + + +def calc_usage_rate_by_tokyo_gas(amount: int): + if not amount: + raise ValueError(f"AmountValueError: {amount}") + + if amount <= 140: + return amount * 33.79 + elif amount <= 350: + over_amount = amount - 140 + return 140 * 33.79 + over_amount * 34.00 + else: + over_amount = amount - 350 + return 140 * 33.79 + 210 * 34.00 + over_amount * 36.53 + + +def calc_usage_rate_by_jxtg_electrictiy(amount: int): + if not amount: + raise ValueError(f"AmountValueError: {amount}") + + if amount <= 120: + return amount * 19.88 + elif amount <= 300: + over_amount = amount - 120 + return 120 * 19.88 + over_amount * 26.48 + elif amount <= 600: + over_amount = amount - 300 + return 120 * 19.88 + 180 * 19.88 + over_amount * 25.08 + else: + over_amount = amount - 600 + return 120 * 33.79 + 180 * 34.00 + 300 * 25.08 + over_amount * 26.15 + + +def _select_base_rate(plan_data: Path, ampare: int): + with open(plan_data, "r") as f: + reader = csv.DictReader(f) + + for row in reader: + + if ampare == int(row["contract"]): + return float(row["price"]) + + else: + raise Exception("NotFoundContractError") + + +def base_rate_by_tepco(ampare: int): + return _select_base_rate(TEPCO_DIR, ampare) + + +def base_rate_by_looop(ampare: int): + return _select_base_rate(LOOOP_DIR, ampare) + + +def base_rate_by_tokyo_gas(ampare: int): + return _select_base_rate(TOKYOGAS_DIR, ampare) + + +def base_rate_by_jxtg_electricity(ampare: int): + return _select_base_rate(JXTG_DIR, ampare) + + +def calc_electric_simurations(contract=10, amount=10): + + simulations = [] + + try: + base_price_by_tempco = base_rate_by_tepco(contract) + usage_price_by_tempco = calc_usage_rate_by_tepco(amount) + total_price_by_tempco = base_price_by_tempco + usage_price_by_tempco + simulation = { + "provider": "東京電力エナジーパートナー", + "plan": "従量電灯B", + "price": f"{total_price_by_tempco}", + } + simulations.append(simulation) + except Exception as e: + print(e) + + try: + base_price_by_looop = base_rate_by_looop(contract) + usage_price_by_looop = calc_usage_rate_by_looop(amount) + total_price_by_looop = base_price_by_looop + usage_price_by_looop + simulation = { + "provider": "Loopでんき", + "plan": "おうちプラン", + "price": f"{total_price_by_looop}", + } + simulations.append(simulation) + except Exception as e: + print(e) + + try: + base_price_by_tokyo_gas = base_rate_by_tokyo_gas(contract) + usage_price_by_tokyo_gas = calc_usage_rate_by_tokyo_gas(amount) + total_price_by_tokyo_gas = base_price_by_tokyo_gas + usage_price_by_tokyo_gas + simulation = { + "provider": "東京ガス ", + "plan": "ずっとも電気1", + "price": f"{total_price_by_tokyo_gas}", + } + simulations.append(simulation) + except Exception as e: + print(e) + + try: + base_price_by_jxtg_electricity = base_rate_by_jxtg_electricity(contract) + usage_price_by_jxtg_electricity = calc_usage_rate_by_jxtg_electrictiy(amount) + total_price_by_jxtg_electricity = ( + base_price_by_jxtg_electricity + usage_price_by_jxtg_electricity + ) + simulation = { + "provider": "JXTGでんき", + "plan": "従量電灯Bたっぷりプラン", + "price": f"{total_price_by_jxtg_electricity}", + } + simulations.append(simulation) + except Exception as e: + print(e) + + if not simulations: + raise Exception("NotFoundPlansError.") + + return simulations + + +if __name__ == "__main__": + calc_electric_simurations() diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/jxtg-electric/plan.csv b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/jxtg-electric/plan.csv new file mode 100644 index 000000000..604773b8d --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/jxtg-electric/plan.csv @@ -0,0 +1,5 @@ +contract,price +30,858.00 +40,1144.00 +50,1430.00 +60,1716.80 \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/looop/plan.csv b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/looop/plan.csv new file mode 100644 index 000000000..6b3c2c5c4 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/looop/plan.csv @@ -0,0 +1,8 @@ +contract,price +10,0 +15,0 +20,0 +30,0 +40,0 +50,0 +60,0 \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tepco/plan.csv b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tepco/plan.csv new file mode 100644 index 000000000..b05370294 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tepco/plan.csv @@ -0,0 +1,8 @@ +contract,price +10,295.24 +15,442.86 +20,590.48 +30,885.72 +40,1180.96 +50,1476.20 +60,1771.44 \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tokyo-gas/plan.csv b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tokyo-gas/plan.csv new file mode 100644 index 000000000..2df96335f --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tokyo-gas/plan.csv @@ -0,0 +1,5 @@ +contract,price +30,885.72 +40,1180.96 +50,1476.20 +60,1771.44 \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py new file mode 100644 index 000000000..a6a67d9bf --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -0,0 +1,38 @@ +from electricity_rate_simulator.core.electric_simurate import ( + calc_usage_rate_by_tepco, + calc_electric_simurations, +) +import pytest + + +class TestCalcPriceByTepco: + + def test_calc_price(self): + + test_amount = 0 + with pytest.raises(ValueError) as e: + calc_usage_rate_by_tepco(test_amount) + + assert str(e.value) == f"AmountValueError: {test_amount}" + + def test_calc_price_in_minimum_rate(self): + test_amount = 120 + assert calc_usage_rate_by_tepco(test_amount) == 120 * 30.00 + + def test_calc_price_within_limit(self): + test_amount = 280 + over = test_amount - 120 + assert calc_usage_rate_by_tepco(test_amount) == 120 * 30.00 + over * 36.60 + + def test_calc_price_by_over_limit(self): + test_amount = 400 + over = test_amount - 300 + assert ( + calc_usage_rate_by_tepco(test_amount) + == 120 * 30.00 + 180 * 36.60 + over * 40.69 + ) + + def test_carc_rate(self): + simurations = calc_electric_simurations() + assert type(simurations) == list + assert len(simurations) == 2 From 1c9352f41f0e92832ff65f1dff3f574831435efa Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Wed, 21 Feb 2024 20:35:56 +0000 Subject: [PATCH 04/21] add electric simurate of api for prototype. --- .../electricity_rate_simulator/app.py | 31 +++ .../electricity-rate-simulator/poetry.lock | 195 +++++++++++++++++- .../electricity-rate-simulator/pyproject.toml | 2 + .../tests/test_api.py | 25 +++ 4 files changed, 252 insertions(+), 1 deletion(-) create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py new file mode 100644 index 000000000..05750f0bf --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -0,0 +1,31 @@ +from fastapi import FastAPI, HTTPException +import uvicorn + + +from electricity_rate_simulator.core.electric_simurate import calc_electric_simurations + +app = FastAPI() + +contracts = [10, 15, 20, 30, 40, 50, 60] + + +@app.get("/") +def get_root(): + return {"app": "electricity-rate-simulator"} + + +@app.get("/simurations") +def electric_simurations_api(contract: int, amount: int): + if not contract or not amount: + raise HTTPException(status_code=404, detail=f"not found parameters: {contract} or {amount}") + + if contract not in contracts: + raise HTTPException(status_code=404, detail=f"target contract is failed: {contract}") + + simurations = calc_electric_simurations(contract, amount) + + return simurations + + +if __name__ == "__main__": + uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock index 732e10d4a..7b4d6e58c 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock @@ -33,6 +33,116 @@ doc = ["Sphinx (>=7)", "packaging", "sphinx-autodoc-typehints (>=1.2.0)", "sphin test = ["anyio[trio]", "coverage[toml] (>=7)", "exceptiongroup (>=1.2.0)", "hypothesis (>=4.0)", "psutil (>=5.9)", "pytest (>=7.0)", "pytest-mock (>=3.6.1)", "trustme", "uvloop (>=0.17)"] trio = ["trio (>=0.23)"] +[[package]] +name = "certifi" +version = "2024.2.2" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.6" +files = [ + {file = "certifi-2024.2.2-py3-none-any.whl", hash = "sha256:dc383c07b76109f368f6106eee2b593b04a011ea4d55f652c6ca24a754d1cdd1"}, + {file = "certifi-2024.2.2.tar.gz", hash = "sha256:0569859f95fc761b18b45ef421b1290a0f65f147e92a1e5eb3e635f9a5e4e66f"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.3.2" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7.0" +files = [ + {file = "charset-normalizer-3.3.2.tar.gz", hash = "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_ppc64le.whl", hash = "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_s390x.whl", hash = "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win32.whl", hash = "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73"}, + {file = "charset_normalizer-3.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_ppc64le.whl", hash = "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_s390x.whl", hash = "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win32.whl", hash = "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab"}, + {file = "charset_normalizer-3.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_s390x.whl", hash = "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win32.whl", hash = "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7"}, + {file = "charset_normalizer-3.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_ppc64le.whl", hash = "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_s390x.whl", hash = "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win32.whl", hash = "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4"}, + {file = "charset_normalizer-3.3.2-cp37-cp37m-win_amd64.whl", hash = "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_ppc64le.whl", hash = "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_s390x.whl", hash = "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win32.whl", hash = "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25"}, + {file = "charset_normalizer-3.3.2-cp38-cp38-win_amd64.whl", hash = "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_ppc64le.whl", hash = "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_s390x.whl", hash = "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win32.whl", hash = "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f"}, + {file = "charset_normalizer-3.3.2-cp39-cp39-win_amd64.whl", hash = "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d"}, + {file = "charset_normalizer-3.3.2-py3-none-any.whl", hash = "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc"}, +] + [[package]] name = "click" version = "8.1.7" @@ -102,6 +212,27 @@ files = [ {file = "h11-0.14.0.tar.gz", hash = "sha256:8f19fbbe99e72420ff35c00b27a34cb9937e902a8b810e2c88300c6f0a3b699d"}, ] +[[package]] +name = "httpcore" +version = "1.0.3" +description = "A minimal low-level HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpcore-1.0.3-py3-none-any.whl", hash = "sha256:9a6a501c3099307d9fd76ac244e08503427679b1e81ceb1d922485e2f2462ad2"}, + {file = "httpcore-1.0.3.tar.gz", hash = "sha256:5c0f9546ad17dac4d0772b0808856eb616eb8b48ce94f49ed819fd6982a8a544"}, +] + +[package.dependencies] +certifi = "*" +h11 = ">=0.13,<0.15" + +[package.extras] +asyncio = ["anyio (>=4.0,<5.0)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] +trio = ["trio (>=0.22.0,<0.24.0)"] + [[package]] name = "httptools" version = "0.6.1" @@ -150,6 +281,30 @@ files = [ [package.extras] test = ["Cython (>=0.29.24,<0.30.0)"] +[[package]] +name = "httpx" +version = "0.26.0" +description = "The next generation HTTP client." +optional = false +python-versions = ">=3.8" +files = [ + {file = "httpx-0.26.0-py3-none-any.whl", hash = "sha256:8915f5a3627c4d47b73e8202457cb28f1266982d1159bd5779d86a80c0eab1cd"}, + {file = "httpx-0.26.0.tar.gz", hash = "sha256:451b55c30d5185ea6b23c2c793abf9bb237d2a7dfb901ced6ff69ad37ec1dfaf"}, +] + +[package.dependencies] +anyio = "*" +certifi = "*" +httpcore = "==1.*" +idna = "*" +sniffio = "*" + +[package.extras] +brotli = ["brotli", "brotlicffi"] +cli = ["click (==8.*)", "pygments (==2.*)", "rich (>=10,<14)"] +http2 = ["h2 (>=3,<5)"] +socks = ["socksio (==1.*)"] + [[package]] name = "idna" version = "3.6" @@ -404,6 +559,27 @@ files = [ {file = "PyYAML-6.0.1.tar.gz", hash = "sha256:bfdf460b1736c775f2ba9f6a92bca30bc2095067b8a9d77876d1fad6cc3b4a43"}, ] +[[package]] +name = "requests" +version = "2.31.0" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.7" +files = [ + {file = "requests-2.31.0-py3-none-any.whl", hash = "sha256:58cd2187c01e70e6e26505bca751777aa9f2ee0b7f4300988b709f44e013003f"}, + {file = "requests-2.31.0.tar.gz", hash = "sha256:942c5a758f98d790eaed1a29cb6eefc7ffb0d1cf7af05c3d2791656dbd6ad1e1"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + [[package]] name = "sniffio" version = "1.3.0" @@ -454,6 +630,23 @@ files = [ {file = "typing_extensions-4.9.0.tar.gz", hash = "sha256:23478f88c37f27d76ac8aee6c905017a143b0b1b886c3c9f66bc2fd94f9f5783"}, ] +[[package]] +name = "urllib3" +version = "2.2.0" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +files = [ + {file = "urllib3-2.2.0-py3-none-any.whl", hash = "sha256:ce3711610ddce217e6d113a2732fafad960a03fd0318c91faa79481e35c11224"}, + {file = "urllib3-2.2.0.tar.gz", hash = "sha256:051d961ad0c62a94e50ecf1af379c3aba230c66c710493493560c0c223c49f20"}, +] + +[package.extras] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "uvicorn" version = "0.27.1" @@ -695,4 +888,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "589139ad1ef5392abb74dd4b1f496259e5f2766795efa1cb2599c6fe3fed3cd9" +content-hash = "f1746a6428e2daa98d55268168f19a3e0edac0fe4ef868ecad776373c950ab39" diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml index 411387e77..f3700ec02 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml @@ -13,6 +13,8 @@ uvicorn = {extras = ["standard"], version = "^0.27.1"} [tool.poetry.group.dev.dependencies] pytest = "^8.0.0" +requests = "^2.31.0" +httpx = "^0.26.0" [build-system] requires = ["poetry-core"] diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py new file mode 100644 index 000000000..e6e37c3a6 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py @@ -0,0 +1,25 @@ +from fastapi.testclient import TestClient +from electricity_rate_simulator.app import app as ele_app + + +test_client = TestClient(ele_app) + + +def test_root_api(): + res = test_client.get("/") + assert res.status_code == 200 + assert res.json() == {"app": "electricity-rate-simulator"} + + +def test_electric_simurations_api(): + params = {"contract": 10, "amount": 100} + res = test_client.get("/simurations", params=params) + assert res.status_code == 200 + assert res.json() == [ + { + "provider": "東京電力エナジーパートナー", + "plan": "従量電灯B", + "price": "3295.24", + }, + {"provider": "Loopでんき", "plan": "おうちプラン", "price": "2640.0"}, + ] From 5117b43d8640a81cd74ce2923b824f63cdea3346 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Wed, 21 Feb 2024 22:31:19 +0000 Subject: [PATCH 05/21] delete plan.csv use for core rogic of before. --- .../data/plovider/jxtg-electric/plan.csv | 5 ----- .../data/plovider/looop/plan.csv | 8 -------- .../data/plovider/tepco/plan.csv | 8 -------- .../data/plovider/tokyo-gas/plan.csv | 5 ----- 4 files changed, 26 deletions(-) delete mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/jxtg-electric/plan.csv delete mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/looop/plan.csv delete mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tepco/plan.csv delete mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tokyo-gas/plan.csv diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/jxtg-electric/plan.csv b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/jxtg-electric/plan.csv deleted file mode 100644 index 604773b8d..000000000 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/jxtg-electric/plan.csv +++ /dev/null @@ -1,5 +0,0 @@ -contract,price -30,858.00 -40,1144.00 -50,1430.00 -60,1716.80 \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/looop/plan.csv b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/looop/plan.csv deleted file mode 100644 index 6b3c2c5c4..000000000 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/looop/plan.csv +++ /dev/null @@ -1,8 +0,0 @@ -contract,price -10,0 -15,0 -20,0 -30,0 -40,0 -50,0 -60,0 \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tepco/plan.csv b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tepco/plan.csv deleted file mode 100644 index b05370294..000000000 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tepco/plan.csv +++ /dev/null @@ -1,8 +0,0 @@ -contract,price -10,295.24 -15,442.86 -20,590.48 -30,885.72 -40,1180.96 -50,1476.20 -60,1771.44 \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tokyo-gas/plan.csv b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tokyo-gas/plan.csv deleted file mode 100644 index 2df96335f..000000000 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/plovider/tokyo-gas/plan.csv +++ /dev/null @@ -1,5 +0,0 @@ -contract,price -30,885.72 -40,1180.96 -50,1476.20 -60,1771.44 \ No newline at end of file From 4e6a41d3cbb4a33a0f4b2f9e7b8054be80344fdd Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Wed, 21 Feb 2024 22:32:00 +0000 Subject: [PATCH 06/21] add plan.json use for modified core rogic. --- .../data/provider/jxtg-electric/plan.json | 16 ++++++++++++++++ .../data/provider/looop/plan.json | 17 +++++++++++++++++ .../data/provider/tepco/plan.json | 18 ++++++++++++++++++ .../data/provider/tokyo-gas/plan.json | 15 +++++++++++++++ 4 files changed, 66 insertions(+) create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/jxtg-electric/plan.json create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/looop/plan.json create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tepco/plan.json create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tokyo-gas/plan.json diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/jxtg-electric/plan.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/jxtg-electric/plan.json new file mode 100644 index 000000000..983b81322 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/jxtg-electric/plan.json @@ -0,0 +1,16 @@ +{ + "provider": "JXTGでんき", + "name": "従量電灯Bたっぷりプラン", + "contracts": [ + { "contract": "30", "price": 858.00}, + { "contract": "40", "price": 1144.00}, + { "contract": "50", "price": 1430.00}, + { "contract": "60", "price": 1716.80} + ], + "usage": [ + { "over": 0, "until": 120, "price": 19.88}, + { "over": 120, "until": 300, "price": 26.48}, + { "over": 300, "until": 600, "price": 25.08}, + { "over": 600, "price": 26.15} + ] +} \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/looop/plan.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/looop/plan.json new file mode 100644 index 000000000..acb359e19 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/looop/plan.json @@ -0,0 +1,17 @@ +{ + "provider": "Loopでんき", + "name": "おうちプラン", + "contracts": [ + { "contract": "10", "price": 0 }, + { "contract": "15", "price": 0 }, + { "contract": "20", "price": 0 }, + { "contract": "30", "price": 0 }, + { "contract": "40", "price": 0 }, + { "contract": "50", "price": 0 }, + { "contract": "60", "price": 0 } + ], + "usage": [ + { "over": 0, "price": 26.4 } + ] + } + \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tepco/plan.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tepco/plan.json new file mode 100644 index 000000000..2ea64928a --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tepco/plan.json @@ -0,0 +1,18 @@ +{ + "provider": "東京電力エナジーパートナー", + "name": "従量電灯B", + "contracts": [ + { "contract": "10", "price": 286.0 }, + { "contract": "15", "price": 429.0 }, + { "contract": "20", "price": 572.0 }, + { "contract": "30", "price": 858.0 }, + { "contract": "40", "price": 1144.0 }, + { "contract": "50", "price": 1430.0 }, + { "contract": "60", "price": 1716.0 } + ], + "usage": [ + { "over": 0, "until": 120, "price": 19.88 }, + { "over": 120, "until": 300, "price": 26.48}, + { "over": 300, "price": 30.57 } + ] +} diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tokyo-gas/plan.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tokyo-gas/plan.json new file mode 100644 index 000000000..d224b55ee --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tokyo-gas/plan.json @@ -0,0 +1,15 @@ +{ + "provider": "東京ガス", + "name": "ずっとも電気1", + "contracts": [ + { "contract": "30", "price": 885.72}, + { "contract": "40", "price": 1180.96}, + { "contract": "50", "price": 1476.20}, + { "contract": "60", "price": 1771.44} + ], + "usage": [ + { "over": 0, "until": 140, "price": 33.79}, + { "over": 140, "until": 350, "price": 34.00}, + { "over": 350, "price": 36.53} + ] +} \ No newline at end of file From 38f6c6363a07f96785f22f856dc141b80db9baed Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Wed, 21 Feb 2024 22:43:13 +0000 Subject: [PATCH 07/21] modify core rogic. --- .../electricity_rate_simulator/app.py | 8 +- .../core/electric_simurate.py | 186 ++++-------------- .../tests/test_api.py | 14 +- .../tests/test_core.py | 180 ++++++++++++++--- 4 files changed, 209 insertions(+), 179 deletions(-) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index 05750f0bf..99f30fdc9 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -15,14 +15,14 @@ def get_root(): @app.get("/simurations") -def electric_simurations_api(contract: int, amount: int): - if not contract or not amount: - raise HTTPException(status_code=404, detail=f"not found parameters: {contract} or {amount}") +def electric_simurations_api(contract: int, usage: int): + if not contract or not usage: + raise HTTPException(status_code=404, detail=f"not found parameters: {contract} or {usage}") if contract not in contracts: raise HTTPException(status_code=404, detail=f"target contract is failed: {contract}") - simurations = calc_electric_simurations(contract, amount) + simurations = calc_electric_simurations(contract, usage) return simurations diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py index 635dbb53b..b75c91067 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py @@ -1,170 +1,68 @@ from pathlib import Path -import csv +import json BASE_DIR = Path( "/workspaces/coding-challenge/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator" ) DATA_DIR = BASE_DIR.joinpath("data") -TEPCO_DIR = DATA_DIR.joinpath("plovider", "tepco", "plan.csv") -LOOOP_DIR = DATA_DIR.joinpath("plovider", "looop", "plan.csv") -JXTG_DIR = DATA_DIR.joinpath("plovider", "jxtg-electric", "plan.csv") -TOKYOGAS_DIR = DATA_DIR.joinpath("plovider", "tokyo-gas", "plan.csv") +PROVIDER_DIR = DATA_DIR.joinpath("provider") -def calc_usage_rate_by_tepco(amount: int): - """電気料金計算 +def calc_plan(profile: Path, contract: int, usage: int): + with open(profile, "r", encoding="utf-8") as f: + plan = json.load(f) + contracts = plan["contracts"] + usages = plan["usage"] + base_price = calc_base_rate(contracts, contract) + usage_price = calc_usage_rate(usages, usage) - e.g.) - 最初の120kWhまで(第1段階料金) 1kWh 30円00銭 - 120kWhをこえ300kWhまで(第2段階料金) 〃 36円60銭 - 上記超過(第3段階料金) 〃 40円69銭 + total_price = int(base_price + usage_price) - Args: - amount (_type_): _description_ - - """ - if not amount: - raise ValueError(f"AmountValueError: {amount}") - - if amount <= 120: - return amount * 30.00 - elif amount <= 300: - over_amount = amount - 120 - return 120 * 30.00 + over_amount * 36.60 - else: - over_amount = amount - 300 - return 120 * 30.00 + 180 * 36.60 + over_amount * 40.69 - - -def calc_usage_rate_by_looop(amount: int): - if not amount: - raise ValueError(f"AmountValueError: {amount}") - - return amount * 26.40 + simuration = { + "provider": plan["provider"], + "plan": plan["name"], + "price": f"{total_price}円", + } + return simuration -def calc_usage_rate_by_tokyo_gas(amount: int): - if not amount: - raise ValueError(f"AmountValueError: {amount}") - if amount <= 140: - return amount * 33.79 - elif amount <= 350: - over_amount = amount - 140 - return 140 * 33.79 + over_amount * 34.00 - else: - over_amount = amount - 350 - return 140 * 33.79 + 210 * 34.00 + over_amount * 36.53 - - -def calc_usage_rate_by_jxtg_electrictiy(amount: int): - if not amount: - raise ValueError(f"AmountValueError: {amount}") - - if amount <= 120: - return amount * 19.88 - elif amount <= 300: - over_amount = amount - 120 - return 120 * 19.88 + over_amount * 26.48 - elif amount <= 600: - over_amount = amount - 300 - return 120 * 19.88 + 180 * 19.88 + over_amount * 25.08 +def calc_base_rate(contracts: list[dict], contract: int): + for row in contracts: + if contract == int(row["contract"]): + return float(row["price"]) else: - over_amount = amount - 600 - return 120 * 33.79 + 180 * 34.00 + 300 * 25.08 + over_amount * 26.15 - - -def _select_base_rate(plan_data: Path, ampare: int): - with open(plan_data, "r") as f: - reader = csv.DictReader(f) + raise Exception("NotFoundContractError") - for row in reader: - if ampare == int(row["contract"]): - return float(row["price"]) - - else: - raise Exception("NotFoundContractError") +def calc_usage_rate(usages: dict, usage: int): + usage_price = 0 + for row in usages: + over = row["over"] + until = row["until"] if "until" in row else float("inf") + price = row["price"] -def base_rate_by_tepco(ampare: int): - return _select_base_rate(TEPCO_DIR, ampare) + if over <= usage: + if usage < until: + usage_price += (usage - over) * price + else: # usage >= until + usage_price += (until - over) * price + return usage_price -def base_rate_by_looop(ampare: int): - return _select_base_rate(LOOOP_DIR, ampare) +def calc_electric_simurations(contract=10, usage=10): -def base_rate_by_tokyo_gas(ampare: int): - return _select_base_rate(TOKYOGAS_DIR, ampare) + simurations = [] + for profile in PROVIDER_DIR.glob("**/plan.json"): + try: + simuration = calc_plan(profile, contract, usage) + simurations.append(simuration) + except Exception as e: + print(e) - -def base_rate_by_jxtg_electricity(ampare: int): - return _select_base_rate(JXTG_DIR, ampare) - - -def calc_electric_simurations(contract=10, amount=10): - - simulations = [] - - try: - base_price_by_tempco = base_rate_by_tepco(contract) - usage_price_by_tempco = calc_usage_rate_by_tepco(amount) - total_price_by_tempco = base_price_by_tempco + usage_price_by_tempco - simulation = { - "provider": "東京電力エナジーパートナー", - "plan": "従量電灯B", - "price": f"{total_price_by_tempco}", - } - simulations.append(simulation) - except Exception as e: - print(e) - - try: - base_price_by_looop = base_rate_by_looop(contract) - usage_price_by_looop = calc_usage_rate_by_looop(amount) - total_price_by_looop = base_price_by_looop + usage_price_by_looop - simulation = { - "provider": "Loopでんき", - "plan": "おうちプラン", - "price": f"{total_price_by_looop}", - } - simulations.append(simulation) - except Exception as e: - print(e) - - try: - base_price_by_tokyo_gas = base_rate_by_tokyo_gas(contract) - usage_price_by_tokyo_gas = calc_usage_rate_by_tokyo_gas(amount) - total_price_by_tokyo_gas = base_price_by_tokyo_gas + usage_price_by_tokyo_gas - simulation = { - "provider": "東京ガス ", - "plan": "ずっとも電気1", - "price": f"{total_price_by_tokyo_gas}", - } - simulations.append(simulation) - except Exception as e: - print(e) - - try: - base_price_by_jxtg_electricity = base_rate_by_jxtg_electricity(contract) - usage_price_by_jxtg_electricity = calc_usage_rate_by_jxtg_electrictiy(amount) - total_price_by_jxtg_electricity = ( - base_price_by_jxtg_electricity + usage_price_by_jxtg_electricity - ) - simulation = { - "provider": "JXTGでんき", - "plan": "従量電灯Bたっぷりプラン", - "price": f"{total_price_by_jxtg_electricity}", - } - simulations.append(simulation) - except Exception as e: - print(e) - - if not simulations: - raise Exception("NotFoundPlansError.") - - return simulations + return simurations if __name__ == "__main__": diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py index e6e37c3a6..21e4b39cb 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py @@ -12,14 +12,16 @@ def test_root_api(): def test_electric_simurations_api(): - params = {"contract": 10, "amount": 100} + params = {"contract": 10, "usage": 100} res = test_client.get("/simurations", params=params) assert res.status_code == 200 - assert res.json() == [ - { + assert res.json() == [{ "provider": "東京電力エナジーパートナー", "plan": "従量電灯B", - "price": "3295.24", + "price": "2274円", }, - {"provider": "Loopでんき", "plan": "おうちプラン", "price": "2640.0"}, - ] + { + "provider": "Loopでんき", + "plan": "おうちプラン", + "price": "2640円", + }] diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index a6a67d9bf..122e7b3f7 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -1,38 +1,168 @@ from electricity_rate_simulator.core.electric_simurate import ( - calc_usage_rate_by_tepco, calc_electric_simurations, + calc_plan, + calc_base_rate, + calc_usage_rate, ) import pytest +import json +from pathlib import Path +BASE_DIR = Path(__file__).parents[1] +DATA_DIR = BASE_DIR.joinpath("electricity_rate_simulator/data") +PROVIDER_DIR = DATA_DIR.joinpath("provider") -class TestCalcPriceByTepco: - def test_calc_price(self): +class TestElectricSimurations: + def test_calc_electric_simurations_1(self): + contract = 10 + usage = 100 + simurations = calc_electric_simurations(contract, usage) + assert simurations == [ + { + "provider": "東京電力エナジーパートナー", + "plan": "従量電灯B", + "price": "2274円", + }, + { + "provider": "Loopでんき", + "plan": "おうちプラン", + "price": "2640円", + }, + ] - test_amount = 0 - with pytest.raises(ValueError) as e: - calc_usage_rate_by_tepco(test_amount) + def test_calc_electric_simurations_2(self): + contract = 30 + usage = 100 + simurations = calc_electric_simurations(contract, usage) + assert simurations == [ + { + "provider": "東京電力エナジーパートナー", + "plan": "従量電灯B", + "price": "2846円", + }, + {"provider": "東京ガス", "plan": "ずっとも電気1", "price": "4264円"}, + {"provider": "Loopでんき", "plan": "おうちプラン", "price": "2640円"}, + { + "provider": "JXTGでんき", + "plan": "従量電灯Bたっぷりプラン", + "price": "2846円", + }, + ] - assert str(e.value) == f"AmountValueError: {test_amount}" - def test_calc_price_in_minimum_rate(self): - test_amount = 120 - assert calc_usage_rate_by_tepco(test_amount) == 120 * 30.00 +class TestElectricSimurationByTepco: + contract = 10 + usage = 100 + profile = PROVIDER_DIR.joinpath("tepco", "plan.json") - def test_calc_price_within_limit(self): - test_amount = 280 - over = test_amount - 120 - assert calc_usage_rate_by_tepco(test_amount) == 120 * 30.00 + over * 36.60 + def test_calc_plan(self): - def test_calc_price_by_over_limit(self): - test_amount = 400 - over = test_amount - 300 - assert ( - calc_usage_rate_by_tepco(test_amount) - == 120 * 30.00 + 180 * 36.60 + over * 40.69 - ) + simuration = calc_plan(self.profile, self.contract, self.usage) + assert simuration == { + "provider": "東京電力エナジーパートナー", + "plan": "従量電灯B", + "price": "2274円", + } - def test_carc_rate(self): - simurations = calc_electric_simurations() - assert type(simurations) == list - assert len(simurations) == 2 + def test_calc_base_rate(self): + with open(self.profile, "r", encoding="utf-8") as f: + plan = json.load(f) + contracts = plan["contracts"] + base_price = calc_base_rate(contracts, self.contract) + assert base_price == 286 + + def test_calc_usage_rate(self): + with open(self.profile, "r", encoding="utf-8") as f: + plan = json.load(f) + usages = plan["usage"] + + usage_price = calc_usage_rate(usages, self.usage) + assert usage_price == 1988 + + +class TestElectricSimurationByLooopElectricity: + contract = 10 + usage = 100 + profile = PROVIDER_DIR.joinpath("looop", "plan.json") + + def test_calc_plan(self): + simuration = calc_plan(self.profile, self.contract, self.usage) + assert simuration == { + "provider": "Loopでんき", + "plan": "おうちプラン", + "price": "2640円", + } + + def test_calc_base_rate(self): + with open(self.profile, "r", encoding="utf-8") as f: + plan = json.load(f) + contracts = plan["contracts"] + base_price = calc_base_rate(contracts, self.contract) + assert base_price == 0 + + def test_calc_usage_rate(self): + with open(self.profile, "r", encoding="utf-8") as f: + plan = json.load(f) + usages = plan["usage"] + + usage_price = calc_usage_rate(usages, self.usage) + assert usage_price == 2640 + + +class TestElectricSimurationByTokyoGas: + contract = 30 + usage = 100 + profile = PROVIDER_DIR.joinpath("tokyo-gas", "plan.json") + + def test_calc_plan(self): + simuration = calc_plan(self.profile, self.contract, self.usage) + assert simuration == { + "provider": "東京ガス", + "plan": "ずっとも電気1", + "price": "4264円", + } + + def test_calc_base_rate(self): + with open(self.profile, "r", encoding="utf-8") as f: + plan = json.load(f) + contracts = plan["contracts"] + base_price = calc_base_rate(contracts, self.contract) + assert base_price == 885.72 + + def test_calc_usage_rate(self): + with open(self.profile, "r", encoding="utf-8") as f: + plan = json.load(f) + usages = plan["usage"] + + usage_price = calc_usage_rate(usages, self.usage) + assert usage_price == 3379 + + +class TestElectricSimurationByJxtgElectricity: + contract = 30 + usage = 100 + profile = PROVIDER_DIR.joinpath("jxtg-electric", "plan.json") + + def test_calc_plan(self): + simuration = calc_plan(self.profile, self.contract, self.usage) + assert simuration == { + "provider": "JXTGでんき", + "plan": "従量電灯Bたっぷりプラン", + "price": "2846円", + } + + def test_calc_base_rate(self): + with open(self.profile, "r", encoding="utf-8") as f: + plan = json.load(f) + contracts = plan["contracts"] + base_price = calc_base_rate(contracts, self.contract) + assert base_price == 858.00 + + def test_calc_usage_rate(self): + with open(self.profile, "r", encoding="utf-8") as f: + plan = json.load(f) + usages = plan["usage"] + + usage_price = calc_usage_rate(usages, self.usage) + assert usage_price == 1988 From b6ad970565a5da011b90a8f3ee0a5b3381a6550e Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Thu, 22 Feb 2024 01:02:15 +0000 Subject: [PATCH 08/21] rename module file and modify import name. --- .../electricity_rate_simulator/app.py | 18 +++++++++++------- ...ectric_simurate.py => electric_simulate.py} | 16 ++++++++-------- .../tests/test_api.py | 4 ++-- .../tests/test_core.py | 18 +++++++++--------- 4 files changed, 30 insertions(+), 26 deletions(-) rename serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/{electric_simurate.py => electric_simulate.py} (84%) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index 99f30fdc9..860e6bd33 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -2,11 +2,11 @@ import uvicorn -from electricity_rate_simulator.core.electric_simurate import calc_electric_simurations +from electricity_rate_simulator.core.electric_simulate import calc_electric_simulations app = FastAPI() -contracts = [10, 15, 20, 30, 40, 50, 60] +NUM_OF_CONTRACTS = [10, 15, 20, 30, 40, 50, 60] @app.get("/") @@ -14,17 +14,21 @@ def get_root(): return {"app": "electricity-rate-simulator"} -@app.get("/simurations") -def electric_simurations_api(contract: int, usage: int): +@app.get("/simulations") +def electric_simulations_api(contract: int, usage: int): if not contract or not usage: raise HTTPException(status_code=404, detail=f"not found parameters: {contract} or {usage}") - if contract not in contracts: + if contract not in NUM_OF_CONTRACTS: raise HTTPException(status_code=404, detail=f"target contract is failed: {contract}") - simurations = calc_electric_simurations(contract, usage) + try: + simulations = calc_electric_simulations(contract, usage) + return simulations + except Exception as e: + raise HTTPException(status_code=500, detail=f"{e}") - return simurations + return simulations if __name__ == "__main__": diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py similarity index 84% rename from serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py rename to serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py index b75c91067..26bcde569 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simurate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py @@ -18,13 +18,13 @@ def calc_plan(profile: Path, contract: int, usage: int): total_price = int(base_price + usage_price) - simuration = { + simulation = { "provider": plan["provider"], "plan": plan["name"], "price": f"{total_price}円", } - return simuration + return simulation def calc_base_rate(contracts: list[dict], contract: int): @@ -52,18 +52,18 @@ def calc_usage_rate(usages: dict, usage: int): return usage_price -def calc_electric_simurations(contract=10, usage=10): +def calc_electric_simulations(contract=10, usage=10): - simurations = [] + simulations = [] for profile in PROVIDER_DIR.glob("**/plan.json"): try: - simuration = calc_plan(profile, contract, usage) - simurations.append(simuration) + simulation = calc_plan(profile, contract, usage) + simulations.append(simulation) except Exception as e: print(e) - return simurations + return simulations if __name__ == "__main__": - calc_electric_simurations() + calc_electric_simulations() diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py index 21e4b39cb..b900e41d7 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py @@ -11,9 +11,9 @@ def test_root_api(): assert res.json() == {"app": "electricity-rate-simulator"} -def test_electric_simurations_api(): +def test_electric_simulations_api(): params = {"contract": 10, "usage": 100} - res = test_client.get("/simurations", params=params) + res = test_client.get("/simulations", params=params) assert res.status_code == 200 assert res.json() == [{ "provider": "東京電力エナジーパートナー", diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index 122e7b3f7..746bffaba 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -1,5 +1,5 @@ -from electricity_rate_simulator.core.electric_simurate import ( - calc_electric_simurations, +from electricity_rate_simulator.core.electric_simulate import ( + calc_electric_simulations, calc_plan, calc_base_rate, calc_usage_rate, @@ -13,12 +13,12 @@ PROVIDER_DIR = DATA_DIR.joinpath("provider") -class TestElectricSimurations: - def test_calc_electric_simurations_1(self): +class TestElectricsimulations: + def test_calc_electric_simulations_1(self): contract = 10 usage = 100 - simurations = calc_electric_simurations(contract, usage) - assert simurations == [ + simulations = calc_electric_simulations(contract, usage) + assert simulations == [ { "provider": "東京電力エナジーパートナー", "plan": "従量電灯B", @@ -31,11 +31,11 @@ def test_calc_electric_simurations_1(self): }, ] - def test_calc_electric_simurations_2(self): + def test_calc_electric_simulations_2(self): contract = 30 usage = 100 - simurations = calc_electric_simurations(contract, usage) - assert simurations == [ + simulations = calc_electric_simulations(contract, usage) + assert simulations == [ { "provider": "東京電力エナジーパートナー", "plan": "従量電灯B", From 25d86f21cbfee299f0c2a831a650ed048544d329 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Fri, 23 Feb 2024 17:33:33 +0000 Subject: [PATCH 09/21] add application error to core logic. --- .../core/electric_simulate.py | 48 ++++- .../core/exception.py | 34 ++++ .../tests/assets/test_plan.json | 19 ++ .../assets/test_plan_invailed_contracts.json | 12 ++ .../assets/test_plan_invailed_usages.json | 16 ++ .../tests/conftest.py | 41 ++++ .../tests/test_core.py | 189 ++++++++++++------ 7 files changed, 294 insertions(+), 65 deletions(-) create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan.json create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py index 26bcde569..338e8748e 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py @@ -1,5 +1,15 @@ -from pathlib import Path import json +from pathlib import Path + +from .exception import ( + InvalidContractError, + InvalidContractsError, + InvalidUsageError, + InvalidUsagesError, + NotFoundContractError, + ElectricSimulationError, + NotFoundProviderError +) BASE_DIR = Path( "/workspaces/coding-challenge/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator" @@ -11,11 +21,17 @@ def calc_plan(profile: Path, contract: int, usage: int): with open(profile, "r", encoding="utf-8") as f: plan = json.load(f) + + if not plan.get("contracts"): + raise InvalidContractsError("Invailed contracts data") contracts = plan["contracts"] + + if not plan.get("usage"): + raise InvalidUsagesError("Invalid usages data") usages = plan["usage"] + base_price = calc_base_rate(contracts, contract) usage_price = calc_usage_rate(usages, usage) - total_price = int(base_price + usage_price) simulation = { @@ -28,15 +44,28 @@ def calc_plan(profile: Path, contract: int, usage: int): def calc_base_rate(contracts: list[dict], contract: int): + + if not contracts: + raise InvalidContractsError("Invailed contracts data") + + if contract < 0: + raise InvalidContractError(f"Invailed number of contract: {contract}") + for row in contracts: if contract == int(row["contract"]): return float(row["price"]) else: - raise Exception("NotFoundContractError") + raise NotFoundContractError(f"Not found number of contract: {contract}") def calc_usage_rate(usages: dict, usage: int): + if not usages: + raise InvalidUsagesError("Invalid usages data") + + if usage < 0: + raise InvalidUsageError(f"Invalid number of usage: {usage}") + usage_price = 0 for row in usages: over = row["over"] @@ -52,15 +81,24 @@ def calc_usage_rate(usages: dict, usage: int): return usage_price -def calc_electric_simulations(contract=10, usage=10): +def calc_electric_simulations(contract: int, usage: int): + + if contract < 0: + raise InvalidContractError(f"Invailed number of contract: {contract}") + + if usage < 0: + raise InvalidUsageError(f"Invalid number of usage: {usage}") simulations = [] for profile in PROVIDER_DIR.glob("**/plan.json"): try: simulation = calc_plan(profile, contract, usage) simulations.append(simulation) - except Exception as e: + except ElectricSimulationError as e: print(e) + + if not simulations: + raise NotFoundProviderError("Not Found providers") return simulations diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py new file mode 100644 index 000000000..913e82e29 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py @@ -0,0 +1,34 @@ +class ElectricSimulationError(Exception): + pass + + +class ElectricSimulateProviderError(ElectricSimulationError): + pass + + +class NotFoundProviderError(ElectricSimulateProviderError): + pass + + +class InvalidContractsError(ElectricSimulateProviderError): + pass + + +class InvalidUsagesError(ElectricSimulateProviderError): + pass + + +class ElectricSimulateClientError(ElectricSimulationError): + pass + + +class InvalidContractError(ElectricSimulateClientError): + pass + + +class NotFoundContractError(ElectricSimulateClientError): + pass + + +class InvalidUsageError(ElectricSimulateClientError): + pass diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan.json new file mode 100644 index 000000000..5d7213c2d --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan.json @@ -0,0 +1,19 @@ +{ + "provider": "Testでんき", + "name": "Testプラン", + "contracts": [ + { "contract": "10", "price": 10 }, + { "contract": "15", "price": 15 }, + { "contract": "20", "price": 20 }, + { "contract": "30", "price": 30 }, + { "contract": "40", "price": 40 }, + { "contract": "50", "price": 50 }, + { "contract": "60", "price": 60 } + ], + "usage": [ + { "over": 0, "until": 100, "price": 10 }, + { "over": 100, "until": 200, "price": 20 }, + { "over": 300, "price": 30 } + ] + } + \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json new file mode 100644 index 000000000..da399d3d4 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json @@ -0,0 +1,12 @@ +{ + "provider": "Invailedでんき", + "name": "Invailedプラン", + "contracts": [ + ], + "usage": [ + { "over": 0, "until": 100, "price": 10 }, + { "over": 100, "until": 200, "price": 20 }, + { "over": 300, "price": 30 } + ] + } + \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json new file mode 100644 index 000000000..dc162475e --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json @@ -0,0 +1,16 @@ +{ + "provider": "Invailedでんき", + "name": "Invailedプラン", + "contracts": [ + { "contract": "10", "price": 10 }, + { "contract": "15", "price": 15 }, + { "contract": "20", "price": 20 }, + { "contract": "30", "price": 30 }, + { "contract": "40", "price": 40 }, + { "contract": "50", "price": 50 }, + { "contract": "60", "price": 60 } + ], + "usage": [ + ] + } + \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py new file mode 100644 index 000000000..18a74d487 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py @@ -0,0 +1,41 @@ +import pytest +import json +from pathlib import Path + + +BASE_DIR = Path(__file__).parents[1] +DATA_DIR = BASE_DIR.joinpath("electricity_rate_simulator", "data") +PROVIDER_DIR = DATA_DIR.joinpath("provider") + +TEST_DIR = BASE_DIR.joinpath("tests") +TEST_PROFILE = TEST_DIR.joinpath("assets", "test_plan.json") +TEST_PROFILE_INVAILED_CONTRACTS = TEST_DIR.joinpath("assets", "test_plan_invailed_contracts.json") +TEST_PROFILE_INVAILED_USAGES = TEST_DIR.joinpath("assets", "test_plan_invailed_usages.json") + + +@pytest.fixture() +def test_plan_by_tepco(): + profile = PROVIDER_DIR.joinpath("tepco", "plan.json") + return _test_provider_file(profile) + + +@pytest.fixture() +def test_plan_by_looop(): + profile = PROVIDER_DIR.joinpath("looop", "plan.json") + return _test_provider_file(profile) + + +@pytest.fixture() +def test_plan_by_tokyogas(): + profile = PROVIDER_DIR.joinpath("tokyo-gas", "plan.json") + return _test_provider_file(profile) + +@pytest.fixture() +def test_plan_by_jxtg_electricity(): + profile = PROVIDER_DIR.joinpath("jxtg-electric", "plan.json") + return _test_provider_file(profile) + + +def _test_provider_file(profile: Path): + with open(profile, "r", encoding="utf-8") as f: + return json.load(f) \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index 746bffaba..8caf62f47 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -4,20 +4,28 @@ calc_base_rate, calc_usage_rate, ) -import pytest -import json -from pathlib import Path -BASE_DIR = Path(__file__).parents[1] -DATA_DIR = BASE_DIR.joinpath("electricity_rate_simulator/data") -PROVIDER_DIR = DATA_DIR.joinpath("provider") +from electricity_rate_simulator.core.exception import ( + InvalidContractError, + InvalidContractsError, + InvalidUsageError, + InvalidUsagesError, + ElectricSimulationError, +) + +from .conftest import ( + PROVIDER_DIR, + TEST_PROFILE_INVAILED_CONTRACTS, + TEST_PROFILE_INVAILED_USAGES, + TEST_PROFILE, +) + +import pytest class TestElectricsimulations: def test_calc_electric_simulations_1(self): - contract = 10 - usage = 100 - simulations = calc_electric_simulations(contract, usage) + simulations = calc_electric_simulations(contract=10, usage=100) assert simulations == [ { "provider": "東京電力エナジーパートナー", @@ -32,9 +40,7 @@ def test_calc_electric_simulations_1(self): ] def test_calc_electric_simulations_2(self): - contract = 30 - usage = 100 - simulations = calc_electric_simulations(contract, usage) + simulations = calc_electric_simulations(contract=30, usage=100) assert simulations == [ { "provider": "東京電力エナジーパートナー", @@ -51,6 +57,77 @@ def test_calc_electric_simulations_2(self): ] +class TestElectricSimurationExceptions: + + def test_calc_electric_simulations_invailed_contract(self): + contract = -1 + usage = 100 + + with pytest.raises(InvalidContractError) as e: + calc_electric_simulations(contract, usage) + + assert str(e.value) == f"Invailed number of contract: {contract}" + + def test_calc_electric_simulations_invailed_usage(self): + contract = 10 + usage = -1 + + with pytest.raises(InvalidUsageError) as e: + calc_electric_simulations(contract, usage) + + assert str(e.value) == f"Invalid number of usage: {usage}" + + def test_calc_plan_invailed_contract(self): + profile = TEST_PROFILE + contract = -1 + usage = 100 + + with pytest.raises(ElectricSimulationError) as e: + calc_plan(profile, contract, usage) + + assert str(e.value) == f"Invailed number of contract: {contract}" + + def test_calc_plan_invailed_usage(self): + profile = TEST_PROFILE + contract = 10 + usage = -1 + + with pytest.raises(ElectricSimulationError) as e: + calc_plan(profile, contract, usage) + + assert str(e.value) == f"Invalid number of usage: {usage}" + + def test_calc_plan_notfound_contract(self): + profile = TEST_PROFILE + contract = 0 + usage = 100 + + with pytest.raises(ElectricSimulationError) as e: + calc_plan(profile, contract, usage) + + assert str(e.value) == f"Not found number of contract: {contract}" + + def test_calc_plan_invailed_contracts_plan(self): + profile = TEST_PROFILE_INVAILED_CONTRACTS + contract = 10 + usage = 10 + + with pytest.raises(InvalidContractsError) as e: + calc_plan(profile, contract, usage) + + assert str(e.value) == "Invailed contracts data" + + def test_calc_plan_invailed_usages_plan(self): + profile = TEST_PROFILE_INVAILED_USAGES + contract = 10 + usage = 10 + + with pytest.raises(InvalidUsagesError) as e: + calc_plan(profile, contract, usage) + + assert str(e.value) == "Invalid usages data" + + class TestElectricSimurationByTepco: contract = 10 usage = 100 @@ -65,20 +142,18 @@ def test_calc_plan(self): "price": "2274円", } - def test_calc_base_rate(self): - with open(self.profile, "r", encoding="utf-8") as f: - plan = json.load(f) - contracts = plan["contracts"] - base_price = calc_base_rate(contracts, self.contract) - assert base_price == 286 + def test_calc_base_rate(self, test_plan_by_tepco): + plan = test_plan_by_tepco + contracts = plan["contracts"] + base_price = calc_base_rate(contracts, self.contract) + assert base_price == 286 - def test_calc_usage_rate(self): - with open(self.profile, "r", encoding="utf-8") as f: - plan = json.load(f) - usages = plan["usage"] + def test_calc_usage_rate(self, test_plan_by_tepco): + plan = test_plan_by_tepco + usages = plan["usage"] - usage_price = calc_usage_rate(usages, self.usage) - assert usage_price == 1988 + usage_price = calc_usage_rate(usages, self.usage) + assert usage_price == 1988 class TestElectricSimurationByLooopElectricity: @@ -94,20 +169,18 @@ def test_calc_plan(self): "price": "2640円", } - def test_calc_base_rate(self): - with open(self.profile, "r", encoding="utf-8") as f: - plan = json.load(f) - contracts = plan["contracts"] - base_price = calc_base_rate(contracts, self.contract) - assert base_price == 0 + def test_calc_base_rate(self, test_plan_by_looop): + plan = test_plan_by_looop + contracts = plan["contracts"] + base_price = calc_base_rate(contracts, self.contract) + assert base_price == 0 - def test_calc_usage_rate(self): - with open(self.profile, "r", encoding="utf-8") as f: - plan = json.load(f) - usages = plan["usage"] + def test_calc_usage_rate(self, test_plan_by_looop): + plan = test_plan_by_looop + usages = plan["usage"] - usage_price = calc_usage_rate(usages, self.usage) - assert usage_price == 2640 + usage_price = calc_usage_rate(usages, self.usage) + assert usage_price == 2640 class TestElectricSimurationByTokyoGas: @@ -123,20 +196,18 @@ def test_calc_plan(self): "price": "4264円", } - def test_calc_base_rate(self): - with open(self.profile, "r", encoding="utf-8") as f: - plan = json.load(f) - contracts = plan["contracts"] - base_price = calc_base_rate(contracts, self.contract) - assert base_price == 885.72 + def test_calc_base_rate(self, test_plan_by_tokyogas): + plan = test_plan_by_tokyogas + contracts = plan["contracts"] + base_price = calc_base_rate(contracts, self.contract) + assert base_price == 885.72 - def test_calc_usage_rate(self): - with open(self.profile, "r", encoding="utf-8") as f: - plan = json.load(f) - usages = plan["usage"] + def test_calc_usage_rate(self, test_plan_by_tokyogas): + plan = test_plan_by_tokyogas + usages = plan["usage"] - usage_price = calc_usage_rate(usages, self.usage) - assert usage_price == 3379 + usage_price = calc_usage_rate(usages, self.usage) + assert usage_price == 3379 class TestElectricSimurationByJxtgElectricity: @@ -152,17 +223,15 @@ def test_calc_plan(self): "price": "2846円", } - def test_calc_base_rate(self): - with open(self.profile, "r", encoding="utf-8") as f: - plan = json.load(f) - contracts = plan["contracts"] - base_price = calc_base_rate(contracts, self.contract) - assert base_price == 858.00 + def test_calc_base_rate(self, test_plan_by_jxtg_electricity): + plan = test_plan_by_jxtg_electricity + contracts = plan["contracts"] + base_price = calc_base_rate(contracts, self.contract) + assert base_price == 858.00 - def test_calc_usage_rate(self): - with open(self.profile, "r", encoding="utf-8") as f: - plan = json.load(f) - usages = plan["usage"] + def test_calc_usage_rate(self, test_plan_by_jxtg_electricity): + plan = test_plan_by_jxtg_electricity + usages = plan["usage"] - usage_price = calc_usage_rate(usages, self.usage) - assert usage_price == 1988 + usage_price = calc_usage_rate(usages, self.usage) + assert usage_price == 1988 From e0368c428fe636c27acde900a903dc54ad8c8453 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Fri, 23 Feb 2024 18:20:49 +0000 Subject: [PATCH 10/21] modify application error for api --- .../electricity_rate_simulator/app.py | 20 +++++++++---------- .../tests/test_api.py | 20 ++++++++++++++++++- 2 files changed, 29 insertions(+), 11 deletions(-) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index 860e6bd33..daaf18a06 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -3,6 +3,7 @@ from electricity_rate_simulator.core.electric_simulate import calc_electric_simulations +from electricity_rate_simulator.core.exception import ElectricSimulationError app = FastAPI() @@ -16,20 +17,19 @@ def get_root(): @app.get("/simulations") def electric_simulations_api(contract: int, usage: int): - if not contract or not usage: - raise HTTPException(status_code=404, detail=f"not found parameters: {contract} or {usage}") - - if contract not in NUM_OF_CONTRACTS: - raise HTTPException(status_code=404, detail=f"target contract is failed: {contract}") + if not contract or contract not in NUM_OF_CONTRACTS: + raise HTTPException( + status_code=400, detail=f"Invailed value of contract: {contract}" + ) + + if usage < 0: + raise HTTPException(status_code=400, detail=f"Invailed value of usage: {usage}") try: - simulations = calc_electric_simulations(contract, usage) - return simulations - except Exception as e: + return calc_electric_simulations(contract, usage) + except ElectricSimulationError as e: raise HTTPException(status_code=500, detail=f"{e}") - return simulations - if __name__ == "__main__": uvicorn.run("app:app", host="0.0.0.0", port=8000, reload=True) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py index b900e41d7..f5ea072da 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py @@ -1,7 +1,6 @@ from fastapi.testclient import TestClient from electricity_rate_simulator.app import app as ele_app - test_client = TestClient(ele_app) @@ -25,3 +24,22 @@ def test_electric_simulations_api(): "plan": "おうちプラン", "price": "2640円", }] + + +def test_electric_simulations_api_invailed_contract(): + contract = 0 + usage = 100 + params = {"contract": contract, "usage": usage} + res = test_client.get("/simulations", params=params) + assert res.status_code == 400 + assert res.json() == {'detail': f'Invailed value of contract: {contract}'} + + +def test_electric_simulations_api_invailed_usage(): + contract = 10 + usage = -1 + params = {"contract": contract, "usage": usage} + res = test_client.get("/simulations", params=params) + assert res.status_code == 400 + assert res.json() == {"detail": f"Invailed value of usage: {usage}"} + From a4018366b39598ab4367c2746576f6708845862f Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Sun, 25 Feb 2024 10:56:49 +0000 Subject: [PATCH 11/21] add class for electric stimulation to corelogic. --- .../electricity_rate_simulator/app.py | 5 +- .../core/electric_simulate.py | 177 +++--- .../core/exception.py | 16 + .../data/provider/tokyo-gas/plan.json | 14 +- .../tests/test_core.py | 536 +++++++++++++----- 5 files changed, 513 insertions(+), 235 deletions(-) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index daaf18a06..c1e36d16d 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -2,7 +2,7 @@ import uvicorn -from electricity_rate_simulator.core.electric_simulate import calc_electric_simulations +from electricity_rate_simulator.core.electric_simulate import ElectricSimulator from electricity_rate_simulator.core.exception import ElectricSimulationError app = FastAPI() @@ -26,7 +26,8 @@ def electric_simulations_api(contract: int, usage: int): raise HTTPException(status_code=400, detail=f"Invailed value of usage: {usage}") try: - return calc_electric_simulations(contract, usage) + electric_simulator = ElectricSimulator() + return electric_simulator.simulate(contract, usage) except ElectricSimulationError as e: raise HTTPException(status_code=500, detail=f"{e}") diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py index 338e8748e..d00c1e172 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py @@ -8,9 +8,16 @@ InvalidUsagesError, NotFoundContractError, ElectricSimulationError, - NotFoundProviderError + NotFoundProviderError, + InvailedProviderError, + InvailedPlanError, + InvailedUsageOverError, + InvailedUsagePriceError, + ElectricSimulateProviderError, + ElectricSimulateClientError, ) + BASE_DIR = Path( "/workspaces/coding-challenge/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator" ) @@ -18,90 +25,108 @@ PROVIDER_DIR = DATA_DIR.joinpath("provider") -def calc_plan(profile: Path, contract: int, usage: int): - with open(profile, "r", encoding="utf-8") as f: - plan = json.load(f) - - if not plan.get("contracts"): - raise InvalidContractsError("Invailed contracts data") - contracts = plan["contracts"] - - if not plan.get("usage"): - raise InvalidUsagesError("Invalid usages data") - usages = plan["usage"] - - base_price = calc_base_rate(contracts, contract) - usage_price = calc_usage_rate(usages, usage) - total_price = int(base_price + usage_price) - - simulation = { - "provider": plan["provider"], - "plan": plan["name"], - "price": f"{total_price}円", - } - - return simulation - - -def calc_base_rate(contracts: list[dict], contract: int): +class ElectricSimulator(object): - if not contracts: - raise InvalidContractsError("Invailed contracts data") + def __init__(self): + pass - if contract < 0: - raise InvalidContractError(f"Invailed number of contract: {contract}") + def _validate_user_data(self, contract: int, usage: int): + if contract <= 0: + raise InvalidContractError(f"Invailed number of contract: {contract}") - for row in contracts: - if contract == int(row["contract"]): - return float(row["price"]) - else: - raise NotFoundContractError(f"Not found number of contract: {contract}") + if usage < 0: + raise InvalidUsageError(f"Invalid number of usage: {usage}") + def simulate(self, contract: int, usage: int): -def calc_usage_rate(usages: dict, usage: int): + simulations = [] - if not usages: - raise InvalidUsagesError("Invalid usages data") - - if usage < 0: - raise InvalidUsageError(f"Invalid number of usage: {usage}") - - usage_price = 0 - for row in usages: - over = row["over"] - until = row["until"] if "until" in row else float("inf") - price = row["price"] - - if over <= usage: - if usage < until: - usage_price += (usage - over) * price - else: # usage >= until - usage_price += (until - over) * price - - return usage_price - - -def calc_electric_simulations(contract: int, usage: int): + try: + self._validate_user_data(contract, usage) + except ElectricSimulateClientError as e: + raise e - if contract < 0: - raise InvalidContractError(f"Invailed number of contract: {contract}") + for profile in PROVIDER_DIR.glob("**/plan.json"): + try: + simulation = self._calculate_electricity_rate(profile, contract, usage) + simulations.append(simulation) + except ElectricSimulationError as e: + print(e) - if usage < 0: - raise InvalidUsageError(f"Invalid number of usage: {usage}") + if not simulations: + raise NotFoundProviderError("Not Found providers") - simulations = [] - for profile in PROVIDER_DIR.glob("**/plan.json"): - try: - simulation = calc_plan(profile, contract, usage) - simulations.append(simulation) - except ElectricSimulationError as e: - print(e) - - if not simulations: - raise NotFoundProviderError("Not Found providers") + return simulations - return simulations + def _validate_profile(self, profile_data: dict): + if not profile_data.get("contracts"): + raise InvalidContractsError("Invailed contracts data") + if not profile_data.get("usage"): + raise InvalidUsagesError("Invalid usages data") -if __name__ == "__main__": - calc_electric_simulations() + if not profile_data.get("provider"): + raise InvailedProviderError("Invalid provider data") + + if not profile_data.get("name"): + raise InvailedPlanError("Invalid plan data") + + def _calculate_electricity_rate(self, profile: Path, contract: int, usage: int): + with open(profile, "r", encoding="utf-8") as f: + profile_data = json.load(f) + + try: + self._validate_profile(profile_data) + contracts = profile_data["contracts"] + usages = profile_data["usage"] + provider: str = profile_data["provider"] + plan: str = profile_data["name"] + + base_price = self._calculate_base_rate(contracts, contract) + usage_price = self._calculate_usage_rate(usages, usage) + + if usage_price == 0: + total_price = int(base_price / 2) + else: + total_price = int(base_price + usage_price) + + simulation = { + "provider": provider, + "plan": plan, + "price": f"{total_price}円", + } + + return simulation + except (ElectricSimulateProviderError, ElectricSimulateClientError) as e: + raise e + + def _calculate_base_rate(self, contracts: list[dict], contract: int): + for row in contracts: + if contract == int(row["contract"]): + return float(row["price"]) + else: + raise NotFoundContractError(f"Not found number of contract: {contract}") + + def _validate_usage_data(self, item: dict): + if item.get("over", None) is None: + raise InvailedUsageOverError("Invailed over data") + + if item.get("price", None) is None: + raise InvailedUsagePriceError("Invailed price data") + + def _calculate_usage_rate(self, usages: dict, usage: int): + + usage_price = 0 + for item in usages: + self._validate_usage_data(item) + over: int = item["over"] + until: int | float = item["until"] if "until" in item else float("inf") + price = item["price"] + + if over <= usage: + if usage < until: + usage_price += (usage - over) * price + else: # usage >= until + usage_price += (until - over) * price + + return usage_price diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py index 913e82e29..40e43d4c2 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py @@ -18,6 +18,22 @@ class InvalidUsagesError(ElectricSimulateProviderError): pass +class InvailedProviderError(ElectricSimulateProviderError): + pass + + +class InvailedPlanError(ElectricSimulateProviderError): + pass + + +class InvailedUsageOverError(InvalidUsagesError): + pass + + +class InvailedUsagePriceError(InvalidUsagesError): + pass + + class ElectricSimulateClientError(ElectricSimulationError): pass diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tokyo-gas/plan.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tokyo-gas/plan.json index d224b55ee..6b21fa4f2 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tokyo-gas/plan.json +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/data/provider/tokyo-gas/plan.json @@ -2,14 +2,14 @@ "provider": "東京ガス", "name": "ずっとも電気1", "contracts": [ - { "contract": "30", "price": 885.72}, - { "contract": "40", "price": 1180.96}, - { "contract": "50", "price": 1476.20}, - { "contract": "60", "price": 1771.44} + { "contract": "30", "price": 858.00}, + { "contract": "40", "price": 1144.00}, + { "contract": "50", "price": 1430.00}, + { "contract": "60", "price": 1716.00} ], "usage": [ - { "over": 0, "until": 140, "price": 33.79}, - { "over": 140, "until": 350, "price": 34.00}, - { "over": 350, "price": 36.53} + { "over": 0, "until": 140, "price": 23.67}, + { "over": 140, "until": 350, "price": 23.88}, + { "over": 350, "price": 26.41} ] } \ No newline at end of file diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index 8caf62f47..f96ef69b2 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -1,9 +1,4 @@ -from electricity_rate_simulator.core.electric_simulate import ( - calc_electric_simulations, - calc_plan, - calc_base_rate, - calc_usage_rate, -) +from electricity_rate_simulator.core.electric_simulate import ElectricSimulator from electricity_rate_simulator.core.exception import ( InvalidContractError, @@ -11,6 +6,14 @@ InvalidUsageError, InvalidUsagesError, ElectricSimulationError, + ElectricSimulateProviderError, + InvailedProviderError, + InvailedPlanError, + ElectricSimulateClientError, + NotFoundProviderError, + NotFoundContractError, + InvailedUsageOverError, + InvailedUsagePriceError, ) from .conftest import ( @@ -21,11 +24,15 @@ ) import pytest +from pytest_mock.plugin import MockerFixture + + +class TestElectricSimulatior: + electric_simurator = ElectricSimulator() -class TestElectricsimulations: - def test_calc_electric_simulations_1(self): - simulations = calc_electric_simulations(contract=10, usage=100) + def test_simulate_by_contract_10A(self): + simulations = self.electric_simurator.simulate(contract=10, usage=100) assert simulations == [ { "provider": "東京電力エナジーパートナー", @@ -39,15 +46,15 @@ def test_calc_electric_simulations_1(self): }, ] - def test_calc_electric_simulations_2(self): - simulations = calc_electric_simulations(contract=30, usage=100) + def test_simulate_by_contract_30A(self): + simulations = self.electric_simurator.simulate(contract=30, usage=100) assert simulations == [ { "provider": "東京電力エナジーパートナー", "plan": "従量電灯B", "price": "2846円", }, - {"provider": "東京ガス", "plan": "ずっとも電気1", "price": "4264円"}, + {"provider": "東京ガス", "plan": "ずっとも電気1", "price": "3225円"}, {"provider": "Loopでんき", "plan": "おうちプラン", "price": "2640円"}, { "provider": "JXTGでんき", @@ -58,180 +65,409 @@ def test_calc_electric_simulations_2(self): class TestElectricSimurationExceptions: - - def test_calc_electric_simulations_invailed_contract(self): - contract = -1 - usage = 100 - - with pytest.raises(InvalidContractError) as e: - calc_electric_simulations(contract, usage) - - assert str(e.value) == f"Invailed number of contract: {contract}" - - def test_calc_electric_simulations_invailed_usage(self): - contract = 10 - usage = -1 - - with pytest.raises(InvalidUsageError) as e: - calc_electric_simulations(contract, usage) - - assert str(e.value) == f"Invalid number of usage: {usage}" - - def test_calc_plan_invailed_contract(self): - profile = TEST_PROFILE - contract = -1 - usage = 100 - - with pytest.raises(ElectricSimulationError) as e: - calc_plan(profile, contract, usage) - - assert str(e.value) == f"Invailed number of contract: {contract}" - - def test_calc_plan_invailed_usage(self): - profile = TEST_PROFILE - contract = 10 - usage = -1 - - with pytest.raises(ElectricSimulationError) as e: - calc_plan(profile, contract, usage) - - assert str(e.value) == f"Invalid number of usage: {usage}" - - def test_calc_plan_notfound_contract(self): - profile = TEST_PROFILE - contract = 0 - usage = 100 - + profile = TEST_PROFILE + contract = 10 + usage = 100 + electric_simuratior = ElectricSimulator() + + def test_simulate_invailed_contract_error(self, mocker: MockerFixture): + err_msg = "dummy InvalidContractError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator.simulate", + side_effect=InvalidContractError(err_msg), + ) with pytest.raises(ElectricSimulationError) as e: - calc_plan(profile, contract, usage) + self.electric_simuratior.simulate(self.contract, self.usage) - assert str(e.value) == f"Not found number of contract: {contract}" + assert str(e.value) == err_msg - def test_calc_plan_invailed_contracts_plan(self): - profile = TEST_PROFILE_INVAILED_CONTRACTS - contract = 10 - usage = 10 - - with pytest.raises(InvalidContractsError) as e: - calc_plan(profile, contract, usage) - - assert str(e.value) == "Invailed contracts data" - - def test_calc_plan_invailed_usages_plan(self): - profile = TEST_PROFILE_INVAILED_USAGES - contract = 10 - usage = 10 - - with pytest.raises(InvalidUsagesError) as e: - calc_plan(profile, contract, usage) - - assert str(e.value) == "Invalid usages data" + def test_simulate_invailed_usage_error(self, mocker: MockerFixture): + err_msg = "dummy InvalidUsageError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator.simulate", + side_effect=InvalidUsageError(err_msg), + ) + with pytest.raises(InvalidUsageError) as e: + self.electric_simuratior.simulate(self.contract, self.usage) + + assert str(e.value) == err_msg + + def test_simulate_notfound_provider_error(self, mocker: MockerFixture): + err_msg = "dummy NotFoundProviderError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator.simulate", + side_effect=NotFoundProviderError("dummy NotFoundProviderError"), + ) + with pytest.raises(NotFoundProviderError) as e: + self.electric_simuratior.simulate() + + assert str(e.value) == err_msg + + def test_calculate_electricity_rate_invailed_contracts_error( + self, mocker: MockerFixture + ): + err_msg = "dummy InvalidContractsError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + side_effect=InvalidContractsError(err_msg), + ) + with pytest.raises(ElectricSimulateProviderError) as e: + self.electric_simuratior._calculate_electricity_rate(self.profile) + assert str(e.value) == err_msg + + def test_calculate_electricity_rate_invailed_usages_error( + self, mocker: MockerFixture + ): + err_msg = "dummy InvalidUsagesError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + side_effect=InvalidUsagesError(err_msg), + ) + with pytest.raises(ElectricSimulateProviderError) as e: + self.electric_simuratior._calculate_electricity_rate(self.profile) + assert str(e.value) == err_msg + + def test_calculate_electricity_rate_invailed_provider_error( + self, mocker: MockerFixture + ): + err_msg = "dummy InvailedProviderError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + side_effect=InvailedProviderError(err_msg), + ) + with pytest.raises(ElectricSimulateProviderError) as e: + self.electric_simuratior._calculate_electricity_rate(self.profile) + + assert str(e.value) == err_msg + + def test_calculate_electricity_rate_invailed_plan_error( + self, mocker: MockerFixture + ): + err_msg = "dummy InvailedPlanError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + side_effect=InvailedPlanError(err_msg), + ) + with pytest.raises(ElectricSimulateProviderError) as e: + self.electric_simuratior._calculate_electricity_rate(self.profile) + + assert str(e.value) == err_msg + + def test_calculate_electricity_rate_invailed_usage_error( + self, mocker: MockerFixture + ): + err_msg = "dummy ElectricSimulateProviderError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + side_effect=ElectricSimulateProviderError(err_msg), + ) + with pytest.raises(ElectricSimulateProviderError) as e: + self.electric_simuratior._calculate_electricity_rate(self.profile) + + assert str(e.value) == err_msg + + def test_calculate_electricity_rate_notfound_contract_error( + self, mocker: MockerFixture + ): + err_msg = "dummy ElectricSimulateClientError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + side_effect=ElectricSimulateClientError(err_msg), + ) + with pytest.raises(ElectricSimulateClientError) as e: + self.electric_simuratior._calculate_electricity_rate(self.profile) + + assert str(e.value) == err_msg + + def test_calculate_base_rate_notfound_contract_error(self, mocker: MockerFixture): + err_msg = "dummy NotFoundContractError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_base_rate", + side_effect=NotFoundContractError(err_msg), + ) + contracts = [{}] + with pytest.raises(NotFoundContractError) as e: + self.electric_simuratior._calculate_base_rate(contracts) + + assert str(e.value) == err_msg + + def test_calculate_usage_rate_invailed_usage_over_error( + self, mocker: MockerFixture + ): + err_msg = "dummy InvailedUsageOverError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_usage_rate", + side_effect=InvailedUsageOverError(err_msg), + ) + contracts = [{}] + with pytest.raises(InvailedUsageOverError) as e: + self.electric_simuratior._calculate_usage_rate(contracts) + + assert str(e.value) == err_msg + + def test_calculate_usage_rate_invailed_usage_price_error( + self, mocker: MockerFixture + ): + err_msg = "dummy InvailedUsagePriceError" + mocker.patch( + "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_usage_rate", + side_effect=InvailedUsagePriceError(err_msg), + ) + contracts = [{}] + with pytest.raises(InvailedUsagePriceError) as e: + self.electric_simuratior._calculate_usage_rate(contracts) + + assert str(e.value) == err_msg class TestElectricSimurationByTepco: - contract = 10 - usage = 100 - profile = PROVIDER_DIR.joinpath("tepco", "plan.json") - - def test_calc_plan(self): - simuration = calc_plan(self.profile, self.contract, self.usage) + electric_simuratior = ElectricSimulator() + + @pytest.mark.parametrize( + "test_contract, test_usage, test_price", + [ + (15, 120, 2814), + (15, 300, 7581), + (15, 350, 9109), + (20, 120, 2957), + (20, 300, 7724), + (20, 350, 9252), + (30, 120, 3243), + (30, 300, 8010), + (30, 350, 9538), + (40, 120, 3529), + (40, 300, 8296), + (40, 350, 9824), + (50, 120, 3815), + (50, 300, 8582), + (50, 350, 10110), + (60, 120, 4101), + (60, 300, 8868), + (60, 350, 10396), + ], + ) + def test_calculate_electricity_rate(self, test_contract, test_usage, test_price): + profile = PROVIDER_DIR.joinpath("tepco", "plan.json") + simuration = self.electric_simuratior._calculate_electricity_rate( + profile, test_contract, test_usage + ) assert simuration == { "provider": "東京電力エナジーパートナー", "plan": "従量電灯B", - "price": "2274円", + "price": f"{test_price}円", } - def test_calc_base_rate(self, test_plan_by_tepco): - plan = test_plan_by_tepco - contracts = plan["contracts"] - base_price = calc_base_rate(contracts, self.contract) - assert base_price == 286 - - def test_calc_usage_rate(self, test_plan_by_tepco): - plan = test_plan_by_tepco - usages = plan["usage"] - - usage_price = calc_usage_rate(usages, self.usage) - assert usage_price == 1988 + @pytest.mark.parametrize( + "test_contract, test_price", + [ + (10, 286.0), + (15, 429.0), + (20, 572.0), + (30, 858.0), + (40, 1144.0), + (50, 1430.0), + (60, 1716.0), + ], + ) + def test_calculate_base_rate(self, test_plan_by_tepco, test_contract, test_price): + contracts = test_plan_by_tepco["contracts"] + base_price = self.electric_simuratior._calculate_base_rate( + contracts, test_contract + ) + assert base_price == test_price + + @pytest.mark.parametrize( + "test_usage, test_price", + [(120, 2385.6), (300, 7152), (350, 8680.5)], + ) + def test_calculate_usage_rate(self, test_plan_by_tepco, test_usage, test_price): + usages = test_plan_by_tepco["usage"] + usage_price = self.electric_simuratior._calculate_usage_rate(usages, test_usage) + assert usage_price == test_price class TestElectricSimurationByLooopElectricity: - contract = 10 - usage = 100 - profile = PROVIDER_DIR.joinpath("looop", "plan.json") - def test_calc_plan(self): - simuration = calc_plan(self.profile, self.contract, self.usage) + electric_simuratior = ElectricSimulator() + + @pytest.mark.parametrize( + "test_contract, test_usage, test_price", + [ + (10, 100, 2640), + (10, 300, 7920), + (10, 500, 13200), + (15, 100, 2640), + (15, 300, 7920), + (15, 500, 13200), + (20, 100, 2640), + (20, 300, 7920), + (20, 500, 13200), + (30, 100, 2640), + (30, 300, 7920), + (30, 500, 13200), + (40, 100, 2640), + (40, 300, 7920), + (40, 500, 13200), + (50, 100, 2640), + (50, 300, 7920), + (50, 500, 13200), + (60, 100, 2640), + (60, 300, 7920), + (60, 500, 13200), + ], + ) + def test_calculate_electricity_rate(self, test_contract, test_usage, test_price): + profile = PROVIDER_DIR.joinpath("looop", "plan.json") + simuration = self.electric_simuratior._calculate_electricity_rate( + profile, test_contract, test_usage + ) assert simuration == { "provider": "Loopでんき", "plan": "おうちプラン", - "price": "2640円", + "price": f"{test_price}円", } - def test_calc_base_rate(self, test_plan_by_looop): - plan = test_plan_by_looop - contracts = plan["contracts"] - base_price = calc_base_rate(contracts, self.contract) - assert base_price == 0 - - def test_calc_usage_rate(self, test_plan_by_looop): - plan = test_plan_by_looop - usages = plan["usage"] - - usage_price = calc_usage_rate(usages, self.usage) - assert usage_price == 2640 + @pytest.mark.parametrize( + "test_contract, test_price", + [(10, 0), (15, 0), (20, 0), (30, 0), (40, 0), (50, 0), (60, 0)], + ) + def test_calculate_base_rate(self, test_plan_by_looop, test_contract, test_price): + contracts = test_plan_by_looop["contracts"] + base_price = self.electric_simuratior._calculate_base_rate( + contracts, test_contract + ) + assert base_price == test_price + + @pytest.mark.parametrize( + "test_usage, test_price", + [(100, 2640), (300, 7920), (500, 13200)], + ) + def test_calculate_usage_rate(self, test_plan_by_looop, test_usage, test_price): + usages = test_plan_by_looop["usage"] + usage_price = self.electric_simuratior._calculate_usage_rate(usages, test_usage) + assert usage_price == test_price class TestElectricSimurationByTokyoGas: - contract = 30 - usage = 100 - profile = PROVIDER_DIR.joinpath("tokyo-gas", "plan.json") - def test_calc_plan(self): - simuration = calc_plan(self.profile, self.contract, self.usage) + electric_simuratior = ElectricSimulator() + + @pytest.mark.parametrize( + "test_contract, test_usage, test_price", + [ + (30, 0, 429), + (30, 140, 4171), + (30, 350, 9186), + (30, 400, 10507), + (40, 0, 572), + (40, 140, 4457), + (40, 350, 9472), + (40, 400, 10793), + (50, 0, 715), + (50, 140, 4743), + (50, 350, 9758), + (50, 400, 11079), + (60, 0, 858), + (60, 140, 5029), + (60, 350, 10044), + (60, 400, 11365), + ], + ) + def test_calculate_electricity_rate(self, test_contract, test_usage, test_price): + profile = PROVIDER_DIR.joinpath("tokyo-gas", "plan.json") + simuration = self.electric_simuratior._calculate_electricity_rate( + profile, test_contract, test_usage + ) assert simuration == { "provider": "東京ガス", "plan": "ずっとも電気1", - "price": "4264円", + "price": f"{test_price}円", } - def test_calc_base_rate(self, test_plan_by_tokyogas): - plan = test_plan_by_tokyogas - contracts = plan["contracts"] - base_price = calc_base_rate(contracts, self.contract) - assert base_price == 885.72 - - def test_calc_usage_rate(self, test_plan_by_tokyogas): - plan = test_plan_by_tokyogas - usages = plan["usage"] - - usage_price = calc_usage_rate(usages, self.usage) - assert usage_price == 3379 + @pytest.mark.parametrize( + "test_contract, test_price", + [(30, 858.00), (40, 1144.00), (50, 1430.00), (60, 1716.00)], + ) + def test_calculate_base_rate( + self, test_plan_by_tokyogas, test_contract, test_price + ): + contracts = test_plan_by_tokyogas["contracts"] + base_price = self.electric_simuratior._calculate_base_rate( + contracts, test_contract + ) + assert base_price == test_price + + @pytest.mark.parametrize( + "test_usage, test_price", + [(0, 0), (140, 3313.8), (350, 8328.6), (400, 9649.1)], + ) + def test_calculate_usage_rate(self, test_plan_by_tokyogas, test_usage, test_price): + usages = test_plan_by_tokyogas["usage"] + usage_price = self.electric_simuratior._calculate_usage_rate(usages, test_usage) + assert usage_price == test_price class TestElectricSimurationByJxtgElectricity: - contract = 30 - usage = 100 - profile = PROVIDER_DIR.joinpath("jxtg-electric", "plan.json") - def test_calc_plan(self): - simuration = calc_plan(self.profile, self.contract, self.usage) + electric_simuratior = ElectricSimulator() + + @pytest.mark.parametrize( + "test_contract, test_usage, test_price", + [ + (30, 0, 429), + (30, 120, 3243), + (30, 300, 8010), + (30, 600, 15534), + (30, 650, 16841), + (40, 0, 572), + (40, 120, 3529), + (40, 300, 8296), + (40, 600, 15820), + (40, 650, 17127), + (50, 0, 715), + (50, 120, 3815), + (50, 300, 8582), + (50, 600, 16106), + (50, 650, 17413), + (60, 0, 858), + (60, 120, 4102), + (60, 300, 8868), + (60, 600, 16392), + (60, 650, 17700), + ], + ) + def test_calculate_electricity_rate(self, test_contract, test_usage, test_price): + profile = PROVIDER_DIR.joinpath("jxtg-electric", "plan.json") + simuration = self.electric_simuratior._calculate_electricity_rate( + profile, test_contract, test_usage + ) assert simuration == { "provider": "JXTGでんき", "plan": "従量電灯Bたっぷりプラン", - "price": "2846円", + "price": f"{test_price}円", } - def test_calc_base_rate(self, test_plan_by_jxtg_electricity): - plan = test_plan_by_jxtg_electricity - contracts = plan["contracts"] - base_price = calc_base_rate(contracts, self.contract) - assert base_price == 858.00 - - def test_calc_usage_rate(self, test_plan_by_jxtg_electricity): - plan = test_plan_by_jxtg_electricity - usages = plan["usage"] - - usage_price = calc_usage_rate(usages, self.usage) - assert usage_price == 1988 + @pytest.mark.parametrize( + "test_contract, test_price", + [(30, 858.00), (40, 1144.00), (50, 1430.00), (60, 1716.80)], + ) + def test_calculate_base_rate( + self, test_plan_by_jxtg_electricity, test_contract, test_price + ): + contracts = test_plan_by_jxtg_electricity["contracts"] + base_price = self.electric_simuratior._calculate_base_rate( + contracts, test_contract + ) + assert base_price == test_price + + @pytest.mark.parametrize( + "test_usage, test_price", + [(0, 0), (120, 2385.6), (300, 7152), (600, 14676), (650, 15983.5)], + ) + def test_calculate_usage_rate( + self, test_plan_by_jxtg_electricity, test_usage, test_price + ): + usages = test_plan_by_jxtg_electricity["usage"] + usage_price = self.electric_simuratior._calculate_usage_rate(usages, test_usage) + assert usage_price == test_price From c085f953b0cfca706d77366ab358cec3a76518f8 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Sun, 25 Feb 2024 20:25:55 +0000 Subject: [PATCH 12/21] add aplication log --- .../electricity_rate_simulator/app.py | 6 ++- .../core/electric_simulate.py | 45 +++++++++++++------ 2 files changed, 37 insertions(+), 14 deletions(-) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index c1e36d16d..e63cb7b5b 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -1,6 +1,6 @@ from fastapi import FastAPI, HTTPException import uvicorn - +import logging from electricity_rate_simulator.core.electric_simulate import ElectricSimulator from electricity_rate_simulator.core.exception import ElectricSimulationError @@ -9,6 +9,9 @@ NUM_OF_CONTRACTS = [10, 15, 20, 30, 40, 50, 60] +lgr = logging.getLogger("uvicorn.app") +lgr.setLevel(logging.INFO) + @app.get("/") def get_root(): @@ -29,6 +32,7 @@ def electric_simulations_api(contract: int, usage: int): electric_simulator = ElectricSimulator() return electric_simulator.simulate(contract, usage) except ElectricSimulationError as e: + lgr.exception(e) raise HTTPException(status_code=500, detail=f"{e}") diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py index d00c1e172..404ce332d 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py @@ -1,30 +1,43 @@ import json +import logging from pathlib import Path from .exception import ( + ElectricSimulateClientError, + ElectricSimulateProviderError, + ElectricSimulationError, + InvailedPlanError, + InvailedProviderError, + InvailedUsageOverError, + InvailedUsagePriceError, InvalidContractError, InvalidContractsError, InvalidUsageError, InvalidUsagesError, NotFoundContractError, - ElectricSimulationError, NotFoundProviderError, - InvailedProviderError, - InvailedPlanError, - InvailedUsageOverError, - InvailedUsagePriceError, - ElectricSimulateProviderError, - ElectricSimulateClientError, ) - -BASE_DIR = Path( - "/workspaces/coding-challenge/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator" -) +BASE_DIR = Path(__file__).parents[1] DATA_DIR = BASE_DIR.joinpath("data") PROVIDER_DIR = DATA_DIR.joinpath("provider") +def setup_logging(debug_mode=False): + lgr = logging.getLogger("uvicorn") + log_format = "%(asctime)s:[%(levelname)s] %(message)s" + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(log_format) + level = logging.DEBUG if debug_mode else logging.INFO + + lgr.setLevel(level) + lgr.addHandler(stream_handler) + return lgr + + +lgr = setup_logging() + + class ElectricSimulator(object): def __init__(self): @@ -44,6 +57,7 @@ def simulate(self, contract: int, usage: int): try: self._validate_user_data(contract, usage) except ElectricSimulateClientError as e: + lgr.exception(e) raise e for profile in PROVIDER_DIR.glob("**/plan.json"): @@ -51,7 +65,7 @@ def simulate(self, contract: int, usage: int): simulation = self._calculate_electricity_rate(profile, contract, usage) simulations.append(simulation) except ElectricSimulationError as e: - print(e) + lgr.exception(e) if not simulations: raise NotFoundProviderError("Not Found providers") @@ -98,6 +112,7 @@ def _calculate_electricity_rate(self, profile: Path, contract: int, usage: int): return simulation except (ElectricSimulateProviderError, ElectricSimulateClientError) as e: + lgr.exception(e) raise e def _calculate_base_rate(self, contracts: list[dict], contract: int): @@ -118,7 +133,11 @@ def _calculate_usage_rate(self, usages: dict, usage: int): usage_price = 0 for item in usages: - self._validate_usage_data(item) + try: + self._validate_usage_data(item) + except InvalidUsagesError as e: + raise e + over: int = item["over"] until: int | float = item["until"] if "until" in item else float("inf") price = item["price"] From 3e125ca226881021b6b7c0a3062721bc63fc9eb6 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Sun, 25 Feb 2024 21:23:11 +0000 Subject: [PATCH 13/21] modify exception name --- .../core/electric_simulate.py | 20 ++++---- .../core/exception.py | 8 ++-- .../assets/test_plan_invailed_contracts.json | 4 +- .../assets/test_plan_invailed_usages.json | 4 +- .../tests/conftest.py | 4 +- .../tests/test_api.py | 8 ++-- .../tests/test_core.py | 46 +++++++++---------- 7 files changed, 47 insertions(+), 47 deletions(-) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py index 404ce332d..237aa15eb 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py @@ -6,10 +6,10 @@ ElectricSimulateClientError, ElectricSimulateProviderError, ElectricSimulationError, - InvailedPlanError, - InvailedProviderError, - InvailedUsageOverError, - InvailedUsagePriceError, + InvalidPlanError, + InvalidProviderError, + InvalidUsageOverError, + InvalidUsagePriceError, InvalidContractError, InvalidContractsError, InvalidUsageError, @@ -45,7 +45,7 @@ def __init__(self): def _validate_user_data(self, contract: int, usage: int): if contract <= 0: - raise InvalidContractError(f"Invailed number of contract: {contract}") + raise InvalidContractError(f"Invalid number of contract: {contract}") if usage < 0: raise InvalidUsageError(f"Invalid number of usage: {usage}") @@ -74,16 +74,16 @@ def simulate(self, contract: int, usage: int): def _validate_profile(self, profile_data: dict): if not profile_data.get("contracts"): - raise InvalidContractsError("Invailed contracts data") + raise InvalidContractsError("Invalid contracts data") if not profile_data.get("usage"): raise InvalidUsagesError("Invalid usages data") if not profile_data.get("provider"): - raise InvailedProviderError("Invalid provider data") + raise InvalidProviderError("Invalid provider data") if not profile_data.get("name"): - raise InvailedPlanError("Invalid plan data") + raise InvalidPlanError("Invalid plan data") def _calculate_electricity_rate(self, profile: Path, contract: int, usage: int): with open(profile, "r", encoding="utf-8") as f: @@ -124,10 +124,10 @@ def _calculate_base_rate(self, contracts: list[dict], contract: int): def _validate_usage_data(self, item: dict): if item.get("over", None) is None: - raise InvailedUsageOverError("Invailed over data") + raise InvalidUsageOverError("Invalid over data") if item.get("price", None) is None: - raise InvailedUsagePriceError("Invailed price data") + raise InvalidUsagePriceError("Invalid price data") def _calculate_usage_rate(self, usages: dict, usage: int): diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py index 40e43d4c2..45e08bee0 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py @@ -18,19 +18,19 @@ class InvalidUsagesError(ElectricSimulateProviderError): pass -class InvailedProviderError(ElectricSimulateProviderError): +class InvalidProviderError(ElectricSimulateProviderError): pass -class InvailedPlanError(ElectricSimulateProviderError): +class InvalidPlanError(ElectricSimulateProviderError): pass -class InvailedUsageOverError(InvalidUsagesError): +class InvalidUsageOverError(InvalidUsagesError): pass -class InvailedUsagePriceError(InvalidUsagesError): +class InvalidUsagePriceError(InvalidUsagesError): pass diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json index da399d3d4..3e1bece4c 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json @@ -1,6 +1,6 @@ { - "provider": "Invailedでんき", - "name": "Invailedプラン", + "provider": "Invalidでんき", + "name": "Invalidプラン", "contracts": [ ], "usage": [ diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json index dc162475e..f898ded79 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json @@ -1,6 +1,6 @@ { - "provider": "Invailedでんき", - "name": "Invailedプラン", + "provider": "Invalidでんき", + "name": "Invalidプラン", "contracts": [ { "contract": "10", "price": 10 }, { "contract": "15", "price": 15 }, diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py index 18a74d487..b75fb53f6 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py @@ -9,8 +9,8 @@ TEST_DIR = BASE_DIR.joinpath("tests") TEST_PROFILE = TEST_DIR.joinpath("assets", "test_plan.json") -TEST_PROFILE_INVAILED_CONTRACTS = TEST_DIR.joinpath("assets", "test_plan_invailed_contracts.json") -TEST_PROFILE_INVAILED_USAGES = TEST_DIR.joinpath("assets", "test_plan_invailed_usages.json") +TEST_PROFILE_INVAILED_CONTRACTS = TEST_DIR.joinpath("assets", "test_plan_invalid_contracts.json") +TEST_PROFILE_INVAILED_USAGES = TEST_DIR.joinpath("assets", "test_plan_invalid_usages.json") @pytest.fixture() diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py index f5ea072da..6bc3164a2 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py @@ -26,20 +26,20 @@ def test_electric_simulations_api(): }] -def test_electric_simulations_api_invailed_contract(): +def test_electric_simulations_api_invalid_contract(): contract = 0 usage = 100 params = {"contract": contract, "usage": usage} res = test_client.get("/simulations", params=params) assert res.status_code == 400 - assert res.json() == {'detail': f'Invailed value of contract: {contract}'} + assert res.json() == {'detail': f'Invalid number of contract: {contract}'} -def test_electric_simulations_api_invailed_usage(): +def test_electric_simulations_api_invalid_usage(): contract = 10 usage = -1 params = {"contract": contract, "usage": usage} res = test_client.get("/simulations", params=params) assert res.status_code == 400 - assert res.json() == {"detail": f"Invailed value of usage: {usage}"} + assert res.json() == {"detail": f"Invalid number of usage: {usage}"} diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index f96ef69b2..8a19bd1ea 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -7,13 +7,13 @@ InvalidUsagesError, ElectricSimulationError, ElectricSimulateProviderError, - InvailedProviderError, - InvailedPlanError, + InvalidProviderError, + InvalidPlanError, ElectricSimulateClientError, NotFoundProviderError, NotFoundContractError, - InvailedUsageOverError, - InvailedUsagePriceError, + InvalidUsageOverError, + InvalidUsagePriceError, ) from .conftest import ( @@ -70,7 +70,7 @@ class TestElectricSimurationExceptions: usage = 100 electric_simuratior = ElectricSimulator() - def test_simulate_invailed_contract_error(self, mocker: MockerFixture): + def test_simulate_invalid_contract_error(self, mocker: MockerFixture): err_msg = "dummy InvalidContractError" mocker.patch( "electricity_rate_simulator.core.electric_simulate.ElectricSimulator.simulate", @@ -81,7 +81,7 @@ def test_simulate_invailed_contract_error(self, mocker: MockerFixture): assert str(e.value) == err_msg - def test_simulate_invailed_usage_error(self, mocker: MockerFixture): + def test_simulate_invalid_usage_error(self, mocker: MockerFixture): err_msg = "dummy InvalidUsageError" mocker.patch( "electricity_rate_simulator.core.electric_simulate.ElectricSimulator.simulate", @@ -103,7 +103,7 @@ def test_simulate_notfound_provider_error(self, mocker: MockerFixture): assert str(e.value) == err_msg - def test_calculate_electricity_rate_invailed_contracts_error( + def test_calculate_electricity_rate_invalid_contracts_error( self, mocker: MockerFixture ): err_msg = "dummy InvalidContractsError" @@ -115,7 +115,7 @@ def test_calculate_electricity_rate_invailed_contracts_error( self.electric_simuratior._calculate_electricity_rate(self.profile) assert str(e.value) == err_msg - def test_calculate_electricity_rate_invailed_usages_error( + def test_calculate_electricity_rate_invalid_usages_error( self, mocker: MockerFixture ): err_msg = "dummy InvalidUsagesError" @@ -127,33 +127,33 @@ def test_calculate_electricity_rate_invailed_usages_error( self.electric_simuratior._calculate_electricity_rate(self.profile) assert str(e.value) == err_msg - def test_calculate_electricity_rate_invailed_provider_error( + def test_calculate_electricity_rate_invalid_provider_error( self, mocker: MockerFixture ): - err_msg = "dummy InvailedProviderError" + err_msg = "dummy InvalidProviderError" mocker.patch( "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", - side_effect=InvailedProviderError(err_msg), + side_effect=InvalidProviderError(err_msg), ) with pytest.raises(ElectricSimulateProviderError) as e: self.electric_simuratior._calculate_electricity_rate(self.profile) assert str(e.value) == err_msg - def test_calculate_electricity_rate_invailed_plan_error( + def test_calculate_electricity_rate_invalid_plan_error( self, mocker: MockerFixture ): - err_msg = "dummy InvailedPlanError" + err_msg = "dummy InvalidPlanError" mocker.patch( "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", - side_effect=InvailedPlanError(err_msg), + side_effect=InvalidPlanError(err_msg), ) with pytest.raises(ElectricSimulateProviderError) as e: self.electric_simuratior._calculate_electricity_rate(self.profile) assert str(e.value) == err_msg - def test_calculate_electricity_rate_invailed_usage_error( + def test_calculate_electricity_rate_invalid_usage_error( self, mocker: MockerFixture ): err_msg = "dummy ElectricSimulateProviderError" @@ -191,30 +191,30 @@ def test_calculate_base_rate_notfound_contract_error(self, mocker: MockerFixture assert str(e.value) == err_msg - def test_calculate_usage_rate_invailed_usage_over_error( + def test_calculate_usage_rate_invalid_usage_over_error( self, mocker: MockerFixture ): - err_msg = "dummy InvailedUsageOverError" + err_msg = "dummy InvalidUsageOverError" mocker.patch( "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_usage_rate", - side_effect=InvailedUsageOverError(err_msg), + side_effect=InvalidUsageOverError(err_msg), ) contracts = [{}] - with pytest.raises(InvailedUsageOverError) as e: + with pytest.raises(InvalidUsageOverError) as e: self.electric_simuratior._calculate_usage_rate(contracts) assert str(e.value) == err_msg - def test_calculate_usage_rate_invailed_usage_price_error( + def test_calculate_usage_rate_invalid_usage_price_error( self, mocker: MockerFixture ): - err_msg = "dummy InvailedUsagePriceError" + err_msg = "dummy InvalidUsagePriceError" mocker.patch( "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_usage_rate", - side_effect=InvailedUsagePriceError(err_msg), + side_effect=InvalidUsagePriceError(err_msg), ) contracts = [{}] - with pytest.raises(InvailedUsagePriceError) as e: + with pytest.raises(InvalidUsagePriceError) as e: self.electric_simuratior._calculate_usage_rate(contracts) assert str(e.value) == err_msg From 4eb6a29913e15a7df3e13bc40373858b39683e26 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Mon, 26 Feb 2024 12:17:26 +0000 Subject: [PATCH 14/21] rename file --- ...n_invailed_contracts.json => test_plan_invalid_contracts.json} | 0 ...st_plan_invailed_usages.json => test_plan_invalid_usages.json} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/{test_plan_invailed_contracts.json => test_plan_invalid_contracts.json} (100%) rename serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/{test_plan_invailed_usages.json => test_plan_invalid_usages.json} (100%) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invalid_contracts.json similarity index 100% rename from serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_contracts.json rename to serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invalid_contracts.json diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invalid_usages.json similarity index 100% rename from serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invailed_usages.json rename to serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/assets/test_plan_invalid_usages.json From ea72e8d8254ada7433d462c6079629d8fc5d322b Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Mon, 26 Feb 2024 13:11:27 +0000 Subject: [PATCH 15/21] add model.py --- .../electricity_rate_simulator/app.py | 19 +- .../core/electric_simulate.py | 176 ++++++++---------- .../core/exception.py | 8 +- .../electricity_rate_simulator/model.py | 81 ++++++++ .../tests/conftest.py | 49 ++++- .../tests/test_core.py | 129 ++++++------- .../tests/test_model.py | 112 +++++++++++ 7 files changed, 384 insertions(+), 190 deletions(-) create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/model.py create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index e63cb7b5b..79b4f90bc 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -4,11 +4,12 @@ from electricity_rate_simulator.core.electric_simulate import ElectricSimulator from electricity_rate_simulator.core.exception import ElectricSimulationError - -app = FastAPI() +from electricity_rate_simulator.model import UserData NUM_OF_CONTRACTS = [10, 15, 20, 30, 40, 50, 60] +app = FastAPI() + lgr = logging.getLogger("uvicorn.app") lgr.setLevel(logging.INFO) @@ -20,17 +21,17 @@ def get_root(): @app.get("/simulations") def electric_simulations_api(contract: int, usage: int): - if not contract or contract not in NUM_OF_CONTRACTS: + + try: + user_data = UserData(contract=contract, usage=usage) + except ElectricSimulationError as e: raise HTTPException( - status_code=400, detail=f"Invailed value of contract: {contract}" - ) - - if usage < 0: - raise HTTPException(status_code=400, detail=f"Invailed value of usage: {usage}") + status_code=400, detail=f"{e}" + ) try: electric_simulator = ElectricSimulator() - return electric_simulator.simulate(contract, usage) + return electric_simulator.simulate(user_data) except ElectricSimulationError as e: lgr.exception(e) raise HTTPException(status_code=500, detail=f"{e}") diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py index 237aa15eb..d0837dada 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py @@ -2,18 +2,13 @@ import logging from pathlib import Path +from pydantic import ValidationError + +from ..model import PlanContract, PlanUsage, ProFile, UserData from .exception import ( ElectricSimulateClientError, ElectricSimulateProviderError, ElectricSimulationError, - InvalidPlanError, - InvalidProviderError, - InvalidUsageOverError, - InvalidUsagePriceError, - InvalidContractError, - InvalidContractsError, - InvalidUsageError, - InvalidUsagesError, NotFoundContractError, NotFoundProviderError, ) @@ -24,7 +19,7 @@ def setup_logging(debug_mode=False): - lgr = logging.getLogger("uvicorn") + lgr = logging.getLogger("uvicorn.app") log_format = "%(asctime)s:[%(levelname)s] %(message)s" stream_handler = logging.StreamHandler() stream_handler.setFormatter(log_format) @@ -43,29 +38,21 @@ class ElectricSimulator(object): def __init__(self): pass - def _validate_user_data(self, contract: int, usage: int): - if contract <= 0: - raise InvalidContractError(f"Invalid number of contract: {contract}") - - if usage < 0: - raise InvalidUsageError(f"Invalid number of usage: {usage}") - - def simulate(self, contract: int, usage: int): + def simulate(self, user_data: UserData): simulations = [] - try: - self._validate_user_data(contract, usage) - except ElectricSimulateClientError as e: - lgr.exception(e) - raise e - for profile in PROVIDER_DIR.glob("**/plan.json"): - try: - simulation = self._calculate_electricity_rate(profile, contract, usage) - simulations.append(simulation) - except ElectricSimulationError as e: - lgr.exception(e) + with open(profile, "r", encoding="utf-8") as f: + provider_data = json.load(f) + try: + provider_data = self._validate_profile(provider_data) + simulation = self._calculate_electricity_rate( + provider_data, user_data + ) + simulations.append(simulation) + except ElectricSimulationError as e: + lgr.exception(e) if not simulations: raise NotFoundProviderError("Not Found providers") @@ -73,79 +60,76 @@ def simulate(self, contract: int, usage: int): return simulations def _validate_profile(self, profile_data: dict): - if not profile_data.get("contracts"): - raise InvalidContractsError("Invalid contracts data") - - if not profile_data.get("usage"): - raise InvalidUsagesError("Invalid usages data") - - if not profile_data.get("provider"): - raise InvalidProviderError("Invalid provider data") - - if not profile_data.get("name"): - raise InvalidPlanError("Invalid plan data") - - def _calculate_electricity_rate(self, profile: Path, contract: int, usage: int): - with open(profile, "r", encoding="utf-8") as f: - profile_data = json.load(f) - - try: - self._validate_profile(profile_data) - contracts = profile_data["contracts"] - usages = profile_data["usage"] - provider: str = profile_data["provider"] - plan: str = profile_data["name"] - - base_price = self._calculate_base_rate(contracts, contract) - usage_price = self._calculate_usage_rate(usages, usage) - - if usage_price == 0: - total_price = int(base_price / 2) - else: - total_price = int(base_price + usage_price) - - simulation = { - "provider": provider, - "plan": plan, - "price": f"{total_price}円", - } - - return simulation - except (ElectricSimulateProviderError, ElectricSimulateClientError) as e: - lgr.exception(e) - raise e - - def _calculate_base_rate(self, contracts: list[dict], contract: int): - for row in contracts: - if contract == int(row["contract"]): - return float(row["price"]) - else: - raise NotFoundContractError(f"Not found number of contract: {contract}") + try: + contracts = [ + PlanContract( + contract=contracts.get("contract"), price=contracts.get("price") + ) + for contracts in profile_data.get("contracts") + ] + usages = [ + PlanUsage( + over=usage.get("over"), + until=usage.get("until"), + price=usage.get("price"), + ) + for usage in profile_data.get("usage") + ] + return ProFile( + provider=profile_data.get("provider"), + plan=profile_data.get("name"), + contracts=contracts, + usage=usages, + ) + except ValidationError as e: + raise ElectricSimulationError(e) + + except ElectricSimulateProviderError as e: + raise e - def _validate_usage_data(self, item: dict): - if item.get("over", None) is None: - raise InvalidUsageOverError("Invalid over data") + def _calculate_electricity_rate(self, provider_data: ProFile, user_data: UserData): + try: + base_price = self._calculate_base_rate( + provider_data.contracts, user_data.contract + ) + usage_price = self._calculate_usage_rate( + provider_data.usage, user_data.usage + ) + + if usage_price == 0: + total_price = int(base_price / 2) + else: + total_price = int(base_price + usage_price) + + simulation = { + "provider": provider_data.provider, + "plan": provider_data.plan, + "price": f"{total_price}円", + } + + return simulation + except (ElectricSimulateProviderError, ElectricSimulateClientError) as e: + lgr.exception(e) + raise e - if item.get("price", None) is None: - raise InvalidUsagePriceError("Invalid price data") + def _calculate_base_rate(self, contracts: list[PlanContract], contract: int): + for item in contracts: + if contract == item.contract: + return item.price + else: + raise NotFoundContractError(f"Not found number of contract: {contract}") - def _calculate_usage_rate(self, usages: dict, usage: int): + def _calculate_usage_rate(self, usages: list[PlanUsage], usage: int): usage_price = 0 - for item in usages: - try: - self._validate_usage_data(item) - except InvalidUsagesError as e: - raise e - - over: int = item["over"] - until: int | float = item["until"] if "until" in item else float("inf") - price = item["price"] - - if over <= usage: - if usage < until: - usage_price += (usage - over) * price + for plan_usage in usages: + + if plan_usage.over <= usage: + if usage < plan_usage.until: + usage_price += (usage - plan_usage.over) * plan_usage.price else: # usage >= until - usage_price += (until - over) * price + usage_price += ( + plan_usage.until - plan_usage.over + ) * plan_usage.price return usage_price diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py index 45e08bee0..462d7e6fe 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py @@ -14,19 +14,19 @@ class InvalidContractsError(ElectricSimulateProviderError): pass -class InvalidUsagesError(ElectricSimulateProviderError): +class InvalidContractPriceError(ElectricSimulateProviderError): pass -class InvalidProviderError(ElectricSimulateProviderError): +class InvalidUsagesError(ElectricSimulateProviderError): pass -class InvalidPlanError(ElectricSimulateProviderError): +class InvalidUsageOverError(InvalidUsagesError): pass -class InvalidUsageOverError(InvalidUsagesError): +class InvalidUsageUntilError(InvalidUsagesError): pass diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/model.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/model.py new file mode 100644 index 000000000..b1200f7c7 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/model.py @@ -0,0 +1,81 @@ +from pydantic import BaseModel, field_validator +from .core.exception import ( + InvalidContractError, + InvalidUsageError, + InvalidContractsError, + InvalidContractPriceError, + InvalidUsageOverError, + InvalidUsageUntilError, + InvalidUsagePriceError, +) + +NUM_OF_CONTRACTS = [10, 15, 20, 30, 40, 50, 60] + + +class PlanUsage(BaseModel): + over: int + until: int | float | None + price: float + + @field_validator("over") + def validate_over(cls, v: int): + if v is None or v < 0: + raise InvalidUsageOverError("Invalid over data") + return v + + @field_validator("until") + def validate_until(cls, v: int | float | None): + + if v is None: + v = float("inf") + + if v < 0: + raise InvalidUsageUntilError("Invalid until data") + return v + + @field_validator("price") + def validate_price(cls, v: float): + if v is None or v < 0: + raise InvalidUsagePriceError("Invalid price data") + return v + + +class PlanContract(BaseModel): + contract: int + price: float + + @field_validator("contract") + def validate_contract(cls, v: int): + if v is None or v < 0: + raise InvalidContractsError("Invalid contracts data") + return v + + @field_validator("price") + def validate_price(cls, v: float): + if v is None or v < 0: + raise InvalidContractPriceError("Invalid price data") + return v + + +class ProFile(BaseModel): + provider: str + plan: str + contracts: list[PlanContract] + usage: list[PlanUsage] + + +class UserData(BaseModel): + contract: int + usage: int + + @field_validator("contract") + def validate_contract(cls, v: int): + if v <= 0 or v not in NUM_OF_CONTRACTS: + raise InvalidContractError(f"Invalid number of contract: {v}") + return v + + @field_validator("usage") + def validate_usage(cls, v: int): + if v < 0: + raise InvalidUsageError(f"Invalid number of usage: {v}") + return v diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py index b75fb53f6..83d96d25c 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py @@ -1,7 +1,7 @@ import pytest import json from pathlib import Path - +from electricity_rate_simulator.model import ProFile, PlanContract, PlanUsage BASE_DIR = Path(__file__).parents[1] DATA_DIR = BASE_DIR.joinpath("electricity_rate_simulator", "data") @@ -9,8 +9,45 @@ TEST_DIR = BASE_DIR.joinpath("tests") TEST_PROFILE = TEST_DIR.joinpath("assets", "test_plan.json") -TEST_PROFILE_INVAILED_CONTRACTS = TEST_DIR.joinpath("assets", "test_plan_invalid_contracts.json") -TEST_PROFILE_INVAILED_USAGES = TEST_DIR.joinpath("assets", "test_plan_invalid_usages.json") +TEST_PROFILE_INVAILED_CONTRACTS = TEST_DIR.joinpath( + "assets", "test_plan_invalid_contracts.json" +) +TEST_PROFILE_INVAILED_USAGES = TEST_DIR.joinpath( + "assets", "test_plan_invalid_usages.json" +) + + +@pytest.fixture() +def test_plan(): + return _test_provider_file(TEST_PROFILE) + + +@pytest.fixture() +def test_usages(): + test_profile = _test_provider_file(TEST_PROFILE) + return test_profile.usage + + +@pytest.fixture() +def test_contracts(): + test_profile = _test_provider_file(TEST_PROFILE) + return test_profile.contracts + +@pytest.fixture() +def test_contracts_invalid_data(): + test_profile = _test_provider_file(TEST_PROFILE_INVAILED_CONTRACTS) + return test_profile.contracts + + +@pytest.fixture() +def test_plan_invlid_usages(): + return _test_provider_file(TEST_PROFILE_INVAILED_USAGES) + + +@pytest.fixture() +def test_usages_invalid_data(): + test_profile = _test_provider_file(TEST_PROFILE_INVAILED_USAGES) + return test_profile.usage @pytest.fixture() @@ -30,6 +67,7 @@ def test_plan_by_tokyogas(): profile = PROVIDER_DIR.joinpath("tokyo-gas", "plan.json") return _test_provider_file(profile) + @pytest.fixture() def test_plan_by_jxtg_electricity(): profile = PROVIDER_DIR.joinpath("jxtg-electric", "plan.json") @@ -38,4 +76,7 @@ def test_plan_by_jxtg_electricity(): def _test_provider_file(profile: Path): with open(profile, "r", encoding="utf-8") as f: - return json.load(f) \ No newline at end of file + profile_data = json.load(f) + contracts = [PlanContract(contract=contracts.get("contract"), price=contracts.get("price")) for contracts in profile_data.get("contracts")] + usages = [PlanUsage(over=usage.get("over"), until=usage.get("until"), price=usage.get("price")) for usage in profile_data.get("usage")] + return ProFile(provider=profile_data.get("provider"), plan=profile_data.get("name"), contracts=contracts, usage=usages) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index 8a19bd1ea..1150234c0 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -1,38 +1,31 @@ +import pytest from electricity_rate_simulator.core.electric_simulate import ElectricSimulator - from electricity_rate_simulator.core.exception import ( + ElectricSimulateClientError, + ElectricSimulateProviderError, + ElectricSimulationError, InvalidContractError, InvalidContractsError, InvalidUsageError, - InvalidUsagesError, - ElectricSimulationError, - ElectricSimulateProviderError, - InvalidProviderError, - InvalidPlanError, - ElectricSimulateClientError, - NotFoundProviderError, - NotFoundContractError, InvalidUsageOverError, InvalidUsagePriceError, + InvalidUsagesError, + NotFoundContractError, + NotFoundProviderError, ) - -from .conftest import ( - PROVIDER_DIR, - TEST_PROFILE_INVAILED_CONTRACTS, - TEST_PROFILE_INVAILED_USAGES, - TEST_PROFILE, -) - -import pytest +from electricity_rate_simulator.model import UserData from pytest_mock.plugin import MockerFixture +from .conftest import TEST_PROFILE + class TestElectricSimulatior: electric_simurator = ElectricSimulator() def test_simulate_by_contract_10A(self): - simulations = self.electric_simurator.simulate(contract=10, usage=100) + user_data = UserData(contract=10, usage=100) + simulations = self.electric_simurator.simulate(user_data) assert simulations == [ { "provider": "東京電力エナジーパートナー", @@ -47,7 +40,8 @@ def test_simulate_by_contract_10A(self): ] def test_simulate_by_contract_30A(self): - simulations = self.electric_simurator.simulate(contract=30, usage=100) + user_data = UserData(contract=30, usage=100) + simulations = self.electric_simurator.simulate(user_data) assert simulations == [ { "provider": "東京電力エナジーパートナー", @@ -127,32 +121,6 @@ def test_calculate_electricity_rate_invalid_usages_error( self.electric_simuratior._calculate_electricity_rate(self.profile) assert str(e.value) == err_msg - def test_calculate_electricity_rate_invalid_provider_error( - self, mocker: MockerFixture - ): - err_msg = "dummy InvalidProviderError" - mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", - side_effect=InvalidProviderError(err_msg), - ) - with pytest.raises(ElectricSimulateProviderError) as e: - self.electric_simuratior._calculate_electricity_rate(self.profile) - - assert str(e.value) == err_msg - - def test_calculate_electricity_rate_invalid_plan_error( - self, mocker: MockerFixture - ): - err_msg = "dummy InvalidPlanError" - mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", - side_effect=InvalidPlanError(err_msg), - ) - with pytest.raises(ElectricSimulateProviderError) as e: - self.electric_simuratior._calculate_electricity_rate(self.profile) - - assert str(e.value) == err_msg - def test_calculate_electricity_rate_invalid_usage_error( self, mocker: MockerFixture ): @@ -191,9 +159,7 @@ def test_calculate_base_rate_notfound_contract_error(self, mocker: MockerFixture assert str(e.value) == err_msg - def test_calculate_usage_rate_invalid_usage_over_error( - self, mocker: MockerFixture - ): + def test_calculate_usage_rate_invalid_usage_over_error(self, mocker: MockerFixture): err_msg = "dummy InvalidUsageOverError" mocker.patch( "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_usage_rate", @@ -247,10 +213,12 @@ class TestElectricSimurationByTepco: (60, 350, 10396), ], ) - def test_calculate_electricity_rate(self, test_contract, test_usage, test_price): - profile = PROVIDER_DIR.joinpath("tepco", "plan.json") + def test_calculate_electricity_rate( + self, test_plan_by_tepco, test_contract, test_usage, test_price + ): + user_data = UserData(contract=test_contract, usage=test_usage) simuration = self.electric_simuratior._calculate_electricity_rate( - profile, test_contract, test_usage + test_plan_by_tepco, user_data ) assert simuration == { "provider": "東京電力エナジーパートナー", @@ -271,9 +239,8 @@ def test_calculate_electricity_rate(self, test_contract, test_usage, test_price) ], ) def test_calculate_base_rate(self, test_plan_by_tepco, test_contract, test_price): - contracts = test_plan_by_tepco["contracts"] base_price = self.electric_simuratior._calculate_base_rate( - contracts, test_contract + test_plan_by_tepco.contracts, test_contract ) assert base_price == test_price @@ -282,8 +249,9 @@ def test_calculate_base_rate(self, test_plan_by_tepco, test_contract, test_price [(120, 2385.6), (300, 7152), (350, 8680.5)], ) def test_calculate_usage_rate(self, test_plan_by_tepco, test_usage, test_price): - usages = test_plan_by_tepco["usage"] - usage_price = self.electric_simuratior._calculate_usage_rate(usages, test_usage) + usage_price = self.electric_simuratior._calculate_usage_rate( + test_plan_by_tepco.usage, test_usage + ) assert usage_price == test_price @@ -317,10 +285,12 @@ class TestElectricSimurationByLooopElectricity: (60, 500, 13200), ], ) - def test_calculate_electricity_rate(self, test_contract, test_usage, test_price): - profile = PROVIDER_DIR.joinpath("looop", "plan.json") + def test_calculate_electricity_rate( + self, test_plan_by_looop, test_contract, test_usage, test_price + ): + user_data = UserData(contract=test_contract, usage=test_usage) simuration = self.electric_simuratior._calculate_electricity_rate( - profile, test_contract, test_usage + test_plan_by_looop, user_data ) assert simuration == { "provider": "Loopでんき", @@ -333,9 +303,8 @@ def test_calculate_electricity_rate(self, test_contract, test_usage, test_price) [(10, 0), (15, 0), (20, 0), (30, 0), (40, 0), (50, 0), (60, 0)], ) def test_calculate_base_rate(self, test_plan_by_looop, test_contract, test_price): - contracts = test_plan_by_looop["contracts"] base_price = self.electric_simuratior._calculate_base_rate( - contracts, test_contract + test_plan_by_looop.contracts, test_contract ) assert base_price == test_price @@ -344,8 +313,9 @@ def test_calculate_base_rate(self, test_plan_by_looop, test_contract, test_price [(100, 2640), (300, 7920), (500, 13200)], ) def test_calculate_usage_rate(self, test_plan_by_looop, test_usage, test_price): - usages = test_plan_by_looop["usage"] - usage_price = self.electric_simuratior._calculate_usage_rate(usages, test_usage) + usage_price = self.electric_simuratior._calculate_usage_rate( + test_plan_by_looop.usage, test_usage + ) assert usage_price == test_price @@ -374,10 +344,12 @@ class TestElectricSimurationByTokyoGas: (60, 400, 11365), ], ) - def test_calculate_electricity_rate(self, test_contract, test_usage, test_price): - profile = PROVIDER_DIR.joinpath("tokyo-gas", "plan.json") + def test_calculate_electricity_rate( + self, test_plan_by_tokyogas, test_contract, test_usage, test_price + ): + user_data = UserData(contract=test_contract, usage=test_usage) simuration = self.electric_simuratior._calculate_electricity_rate( - profile, test_contract, test_usage + test_plan_by_tokyogas, user_data ) assert simuration == { "provider": "東京ガス", @@ -392,9 +364,9 @@ def test_calculate_electricity_rate(self, test_contract, test_usage, test_price) def test_calculate_base_rate( self, test_plan_by_tokyogas, test_contract, test_price ): - contracts = test_plan_by_tokyogas["contracts"] + base_price = self.electric_simuratior._calculate_base_rate( - contracts, test_contract + test_plan_by_tokyogas.contracts, test_contract ) assert base_price == test_price @@ -403,8 +375,9 @@ def test_calculate_base_rate( [(0, 0), (140, 3313.8), (350, 8328.6), (400, 9649.1)], ) def test_calculate_usage_rate(self, test_plan_by_tokyogas, test_usage, test_price): - usages = test_plan_by_tokyogas["usage"] - usage_price = self.electric_simuratior._calculate_usage_rate(usages, test_usage) + usage_price = self.electric_simuratior._calculate_usage_rate( + test_plan_by_tokyogas.usage, test_usage + ) assert usage_price == test_price @@ -437,10 +410,12 @@ class TestElectricSimurationByJxtgElectricity: (60, 650, 17700), ], ) - def test_calculate_electricity_rate(self, test_contract, test_usage, test_price): - profile = PROVIDER_DIR.joinpath("jxtg-electric", "plan.json") + def test_calculate_electricity_rate( + self, test_plan_by_jxtg_electricity, test_contract, test_usage, test_price + ): + user_data = UserData(contract=test_contract, usage=test_usage) simuration = self.electric_simuratior._calculate_electricity_rate( - profile, test_contract, test_usage + test_plan_by_jxtg_electricity, user_data ) assert simuration == { "provider": "JXTGでんき", @@ -455,9 +430,8 @@ def test_calculate_electricity_rate(self, test_contract, test_usage, test_price) def test_calculate_base_rate( self, test_plan_by_jxtg_electricity, test_contract, test_price ): - contracts = test_plan_by_jxtg_electricity["contracts"] base_price = self.electric_simuratior._calculate_base_rate( - contracts, test_contract + test_plan_by_jxtg_electricity.contracts, test_contract ) assert base_price == test_price @@ -468,6 +442,7 @@ def test_calculate_base_rate( def test_calculate_usage_rate( self, test_plan_by_jxtg_electricity, test_usage, test_price ): - usages = test_plan_by_jxtg_electricity["usage"] - usage_price = self.electric_simuratior._calculate_usage_rate(usages, test_usage) + usage_price = self.electric_simuratior._calculate_usage_rate( + test_plan_by_jxtg_electricity.usage, test_usage + ) assert usage_price == test_price diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py new file mode 100644 index 000000000..0d4aed47c --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py @@ -0,0 +1,112 @@ +from electricity_rate_simulator.model import UserData, PlanUsage, PlanContract + +import pytest + +from electricity_rate_simulator.core.exception import ( + InvalidContractError, + InvalidContractsError, + InvalidUsageError, + InvalidUsageOverError, + InvalidUsageUntilError, + InvalidUsagePriceError, + InvalidContractPriceError, +) + + +class TestUserData: + + def test_user_data(self): + user_data = UserData(contract=10, usage=10) + assert user_data.contract == 10 + assert user_data.usage == 10 + + def test_contract_invalid_data(self): + test_contract = 0 + err_msg = f"Invalid number of contract: {test_contract}" + with pytest.raises(InvalidContractError) as e: + UserData(contract=test_contract, usage=10) + + assert str(e.value) == err_msg + + def test_usage_invalid_data(self): + test_usage = -1 + err_msg = f"Invalid number of usage: {test_usage}" + with pytest.raises(InvalidUsageError) as e: + UserData(contract=10, usage=test_usage) + + assert str(e.value) == err_msg + + +class TestPlanUsage: + @pytest.mark.parametrize( + "test_over, test_until, test_price", + [((0, 100, 300), (100, 200, float("inf")), (10, 20, 30))], + ) + def test_plan_usage(self, test_usages, test_over, test_until, test_price): + for item in test_usages: + assert item.over in test_over + assert item.until in test_until + assert item.price in test_price + + def test_plan_usage_invalid_over(self): + err_msg = "Invalid over data" + with pytest.raises(InvalidUsageOverError) as e: + PlanUsage(over=-1, until=0, price=0) + assert str(e.value) == err_msg + + def test_plan_usage_invalid_until(self): + err_msg = "Invalid until data" + with pytest.raises(InvalidUsageUntilError) as e: + PlanUsage(over=0, until=-1, price=0) + assert str(e.value) == err_msg + + def test_plan_usage_invalid_price(self): + err_msg = "Invalid price data" + with pytest.raises(InvalidUsagePriceError) as e: + PlanUsage(over=0, until=0, price=-1) + assert str(e.value) == err_msg + + +class TestPlanContract: + @pytest.mark.parametrize( + "test_contract, test_price", + [((10, 15, 20, 30, 40, 50, 60), (10, 15, 20, 30, 40, 50, 60))], + ) + def test_plan_contarct(self, test_contracts, test_contract, test_price): + for items in test_contracts: + assert items.contract in test_contract + assert items.price in test_price + + def test_plan_contract_invalid_contracts(self): + err_msg = "Invalid contracts data" + with pytest.raises(InvalidContractsError) as e: + PlanContract(contract=-1, price=10) + assert str(e.value) == err_msg + + def test_plan_contract_invalid_price(self): + err_msg = "Invalid price data" + with pytest.raises(InvalidContractPriceError) as e: + PlanContract(contract=10, price=-1) + assert str(e.value) == err_msg + + +class TestProFile: + + def test_profile(self, test_plan): + assert test_plan.provider == "Testでんき" + assert test_plan.plan == "Testプラン" + assert test_plan.contracts == [ + PlanContract(contract=10, price=10), + PlanContract(contract=15, price=15), + PlanContract(contract=20, price=20), + PlanContract(contract=30, price=30), + PlanContract(contract=40, price=40), + PlanContract(contract=50, price=50), + PlanContract(contract=60, price=60), + ] + assert test_plan.usage == [ + PlanUsage(over=0, until=100, price=10), + PlanUsage(over=100, until=200, price=20), + PlanUsage(over=300, until=None, price=30), + ] + From aea7e1bdfcd53643881aab01b94daa3f33f1a3f3 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Mon, 26 Feb 2024 13:15:29 +0000 Subject: [PATCH 16/21] move exception.py --- .../electricity_rate_simulator/app.py | 2 +- .../electricity_rate_simulator/core/electric_simulate.py | 2 +- .../electricity_rate_simulator/{core => }/exception.py | 0 .../electricity_rate_simulator/model.py | 2 +- .../yukikawamura/electricity-rate-simulator/tests/test_core.py | 2 +- .../yukikawamura/electricity-rate-simulator/tests/test_model.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/{core => }/exception.py (100%) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index 79b4f90bc..954dc04c9 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -3,7 +3,7 @@ import logging from electricity_rate_simulator.core.electric_simulate import ElectricSimulator -from electricity_rate_simulator.core.exception import ElectricSimulationError +from electricity_rate_simulator.exception import ElectricSimulationError from electricity_rate_simulator.model import UserData NUM_OF_CONTRACTS = [10, 15, 20, 30, 40, 50, 60] diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py index d0837dada..d8179baa7 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py @@ -5,7 +5,7 @@ from pydantic import ValidationError from ..model import PlanContract, PlanUsage, ProFile, UserData -from .exception import ( +from ..exception import ( ElectricSimulateClientError, ElectricSimulateProviderError, ElectricSimulationError, diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/exception.py similarity index 100% rename from serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/exception.py rename to serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/exception.py diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/model.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/model.py index b1200f7c7..d24f391a4 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/model.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/model.py @@ -1,5 +1,5 @@ from pydantic import BaseModel, field_validator -from .core.exception import ( +from .exception import ( InvalidContractError, InvalidUsageError, InvalidContractsError, diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index 1150234c0..172887081 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -1,6 +1,6 @@ import pytest from electricity_rate_simulator.core.electric_simulate import ElectricSimulator -from electricity_rate_simulator.core.exception import ( +from electricity_rate_simulator.exception import ( ElectricSimulateClientError, ElectricSimulateProviderError, ElectricSimulationError, diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py index 0d4aed47c..90f5ba003 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py @@ -2,7 +2,7 @@ import pytest -from electricity_rate_simulator.core.exception import ( +from electricity_rate_simulator.exception import ( InvalidContractError, InvalidContractsError, InvalidUsageError, From 3b980211d52d914eb05a3d17b3859104c5d2d25e Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Mon, 26 Feb 2024 13:22:06 +0000 Subject: [PATCH 17/21] modify electric_simulate.py to core.py --- .../electricity_rate_simulator/app.py | 2 +- .../{core/electric_simulate.py => core.py} | 6 ++--- .../core/__init__.py | 0 .../tests/test_core.py | 22 +++++++++---------- 4 files changed, 15 insertions(+), 15 deletions(-) rename serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/{core/electric_simulate.py => core.py} (97%) delete mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/__init__.py diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index 954dc04c9..ae71ad1a1 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -2,7 +2,7 @@ import uvicorn import logging -from electricity_rate_simulator.core.electric_simulate import ElectricSimulator +from electricity_rate_simulator.core import ElectricSimulator from electricity_rate_simulator.exception import ElectricSimulationError from electricity_rate_simulator.model import UserData diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py similarity index 97% rename from serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py rename to serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py index d8179baa7..4c7eb7668 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/electric_simulate.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py @@ -4,8 +4,8 @@ from pydantic import ValidationError -from ..model import PlanContract, PlanUsage, ProFile, UserData -from ..exception import ( +from .model import PlanContract, PlanUsage, ProFile, UserData +from .exception import ( ElectricSimulateClientError, ElectricSimulateProviderError, ElectricSimulationError, @@ -13,7 +13,7 @@ NotFoundProviderError, ) -BASE_DIR = Path(__file__).parents[1] +BASE_DIR = Path(__file__).parents[0] DATA_DIR = BASE_DIR.joinpath("data") PROVIDER_DIR = DATA_DIR.joinpath("provider") diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/__init__.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index 172887081..57a0cb619 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -1,5 +1,5 @@ import pytest -from electricity_rate_simulator.core.electric_simulate import ElectricSimulator +from electricity_rate_simulator.core import ElectricSimulator from electricity_rate_simulator.exception import ( ElectricSimulateClientError, ElectricSimulateProviderError, @@ -67,7 +67,7 @@ class TestElectricSimurationExceptions: def test_simulate_invalid_contract_error(self, mocker: MockerFixture): err_msg = "dummy InvalidContractError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator.simulate", + "electricity_rate_simulator.core.ElectricSimulator.simulate", side_effect=InvalidContractError(err_msg), ) with pytest.raises(ElectricSimulationError) as e: @@ -78,7 +78,7 @@ def test_simulate_invalid_contract_error(self, mocker: MockerFixture): def test_simulate_invalid_usage_error(self, mocker: MockerFixture): err_msg = "dummy InvalidUsageError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator.simulate", + "electricity_rate_simulator.core.ElectricSimulator.simulate", side_effect=InvalidUsageError(err_msg), ) with pytest.raises(InvalidUsageError) as e: @@ -89,7 +89,7 @@ def test_simulate_invalid_usage_error(self, mocker: MockerFixture): def test_simulate_notfound_provider_error(self, mocker: MockerFixture): err_msg = "dummy NotFoundProviderError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator.simulate", + "electricity_rate_simulator.core.ElectricSimulator.simulate", side_effect=NotFoundProviderError("dummy NotFoundProviderError"), ) with pytest.raises(NotFoundProviderError) as e: @@ -102,7 +102,7 @@ def test_calculate_electricity_rate_invalid_contracts_error( ): err_msg = "dummy InvalidContractsError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + "electricity_rate_simulator.core.ElectricSimulator._calculate_electricity_rate", side_effect=InvalidContractsError(err_msg), ) with pytest.raises(ElectricSimulateProviderError) as e: @@ -114,7 +114,7 @@ def test_calculate_electricity_rate_invalid_usages_error( ): err_msg = "dummy InvalidUsagesError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + "electricity_rate_simulator.core.ElectricSimulator._calculate_electricity_rate", side_effect=InvalidUsagesError(err_msg), ) with pytest.raises(ElectricSimulateProviderError) as e: @@ -126,7 +126,7 @@ def test_calculate_electricity_rate_invalid_usage_error( ): err_msg = "dummy ElectricSimulateProviderError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + "electricity_rate_simulator.core.ElectricSimulator._calculate_electricity_rate", side_effect=ElectricSimulateProviderError(err_msg), ) with pytest.raises(ElectricSimulateProviderError) as e: @@ -139,7 +139,7 @@ def test_calculate_electricity_rate_notfound_contract_error( ): err_msg = "dummy ElectricSimulateClientError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_electricity_rate", + "electricity_rate_simulator.core.ElectricSimulator._calculate_electricity_rate", side_effect=ElectricSimulateClientError(err_msg), ) with pytest.raises(ElectricSimulateClientError) as e: @@ -150,7 +150,7 @@ def test_calculate_electricity_rate_notfound_contract_error( def test_calculate_base_rate_notfound_contract_error(self, mocker: MockerFixture): err_msg = "dummy NotFoundContractError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_base_rate", + "electricity_rate_simulator.core.ElectricSimulator._calculate_base_rate", side_effect=NotFoundContractError(err_msg), ) contracts = [{}] @@ -162,7 +162,7 @@ def test_calculate_base_rate_notfound_contract_error(self, mocker: MockerFixture def test_calculate_usage_rate_invalid_usage_over_error(self, mocker: MockerFixture): err_msg = "dummy InvalidUsageOverError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_usage_rate", + "electricity_rate_simulator.core.ElectricSimulator._calculate_usage_rate", side_effect=InvalidUsageOverError(err_msg), ) contracts = [{}] @@ -176,7 +176,7 @@ def test_calculate_usage_rate_invalid_usage_price_error( ): err_msg = "dummy InvalidUsagePriceError" mocker.patch( - "electricity_rate_simulator.core.electric_simulate.ElectricSimulator._calculate_usage_rate", + "electricity_rate_simulator.core.ElectricSimulator._calculate_usage_rate", side_effect=InvalidUsagePriceError(err_msg), ) contracts = [{}] From 004f744b9a4040c215a5839ce559036c3c1c3352 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Mon, 26 Feb 2024 14:07:27 +0000 Subject: [PATCH 18/21] add utils.py --- .../electricity_rate_simulator/app.py | 16 +++------ .../electricity_rate_simulator/core.py | 26 +++------------ .../electricity_rate_simulator/utils.py | 19 +++++++++++ .../tests/test_api.py | 33 ++++++++++++++++--- 4 files changed, 57 insertions(+), 37 deletions(-) create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/utils.py diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index ae71ad1a1..6275506b5 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -1,17 +1,13 @@ -from fastapi import FastAPI, HTTPException import uvicorn -import logging - from electricity_rate_simulator.core import ElectricSimulator from electricity_rate_simulator.exception import ElectricSimulationError from electricity_rate_simulator.model import UserData - -NUM_OF_CONTRACTS = [10, 15, 20, 30, 40, 50, 60] +from electricity_rate_simulator.utils import setup_logging +from fastapi import FastAPI, HTTPException app = FastAPI() -lgr = logging.getLogger("uvicorn.app") -lgr.setLevel(logging.INFO) +lgr = setup_logging() @app.get("/") @@ -21,13 +17,11 @@ def get_root(): @app.get("/simulations") def electric_simulations_api(contract: int, usage: int): - + try: user_data = UserData(contract=contract, usage=usage) except ElectricSimulationError as e: - raise HTTPException( - status_code=400, detail=f"{e}" - ) + raise HTTPException(status_code=400, detail=f"{e}") try: electric_simulator = ElectricSimulator() diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py index 4c7eb7668..6dae83196 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py @@ -1,10 +1,7 @@ import json -import logging -from pathlib import Path from pydantic import ValidationError -from .model import PlanContract, PlanUsage, ProFile, UserData from .exception import ( ElectricSimulateClientError, ElectricSimulateProviderError, @@ -12,23 +9,8 @@ NotFoundContractError, NotFoundProviderError, ) - -BASE_DIR = Path(__file__).parents[0] -DATA_DIR = BASE_DIR.joinpath("data") -PROVIDER_DIR = DATA_DIR.joinpath("provider") - - -def setup_logging(debug_mode=False): - lgr = logging.getLogger("uvicorn.app") - log_format = "%(asctime)s:[%(levelname)s] %(message)s" - stream_handler = logging.StreamHandler() - stream_handler.setFormatter(log_format) - level = logging.DEBUG if debug_mode else logging.INFO - - lgr.setLevel(level) - lgr.addHandler(stream_handler) - return lgr - +from .model import PlanContract, PlanUsage, ProFile, UserData +from .utils import PROVIDER_DIR, setup_logging lgr = setup_logging() @@ -83,7 +65,7 @@ def _validate_profile(self, profile_data: dict): ) except ValidationError as e: raise ElectricSimulationError(e) - + except ElectricSimulateProviderError as e: raise e @@ -108,7 +90,7 @@ def _calculate_electricity_rate(self, provider_data: ProFile, user_data: UserDat } return simulation - except (ElectricSimulateProviderError, ElectricSimulateClientError) as e: + except ElectricSimulateClientError as e: lgr.exception(e) raise e diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/utils.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/utils.py new file mode 100644 index 000000000..cbfcd7b65 --- /dev/null +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/utils.py @@ -0,0 +1,19 @@ +from pathlib import Path +import logging + + +BASE_DIR = Path(__file__).parents[0] +DATA_DIR = BASE_DIR.joinpath("data") +PROVIDER_DIR = DATA_DIR.joinpath("provider") + + +def setup_logging(debug_mode=False): + lgr = logging.getLogger("uvicorn.app") + log_format = "%(asctime)s:[%(levelname)s] %(message)s" + stream_handler = logging.StreamHandler() + stream_handler.setFormatter(log_format) + level = logging.DEBUG if debug_mode else logging.INFO + + lgr.setLevel(level) + lgr.addHandler(stream_handler) + return lgr diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py index 6bc3164a2..4c83cc933 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_api.py @@ -14,7 +14,8 @@ def test_electric_simulations_api(): params = {"contract": 10, "usage": 100} res = test_client.get("/simulations", params=params) assert res.status_code == 200 - assert res.json() == [{ + assert res.json() == [ + { "provider": "東京電力エナジーパートナー", "plan": "従量電灯B", "price": "2274円", @@ -23,7 +24,8 @@ def test_electric_simulations_api(): "provider": "Loopでんき", "plan": "おうちプラン", "price": "2640円", - }] + }, + ] def test_electric_simulations_api_invalid_contract(): @@ -32,8 +34,8 @@ def test_electric_simulations_api_invalid_contract(): params = {"contract": contract, "usage": usage} res = test_client.get("/simulations", params=params) assert res.status_code == 400 - assert res.json() == {'detail': f'Invalid number of contract: {contract}'} - + assert res.json() == {"detail": f"Invalid number of contract: {contract}"} + def test_electric_simulations_api_invalid_usage(): contract = 10 @@ -43,3 +45,26 @@ def test_electric_simulations_api_invalid_usage(): assert res.status_code == 400 assert res.json() == {"detail": f"Invalid number of usage: {usage}"} + +def test_electric_simulations_api_invalid_contract_value(): + contract = "test" + usage = 100 + params = {"contract": contract, "usage": usage} + res = test_client.get("/simulations", params=params) + assert res.status_code == 422 + assert ( + res.text + == '{"detail":[{"type":"int_parsing","loc":["query","contract"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"test","url":"https://errors.pydantic.dev/2.6/v/int_parsing"}]}' + ) + + +def test_electric_simulations_api_invalid_usage_value(): + contract = 10 + usage = "test" + params = {"contract": contract, "usage": usage} + res = test_client.get("/simulations", params=params) + assert res.status_code == 422 + assert ( + res.text + == '{"detail":[{"type":"int_parsing","loc":["query","usage"],"msg":"Input should be a valid integer, unable to parse string as an integer","input":"test","url":"https://errors.pydantic.dev/2.6/v/int_parsing"}]}' + ) From 145d95adbe5277bbe91166ac9341fa4a27a1c7f6 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Mon, 26 Feb 2024 14:08:03 +0000 Subject: [PATCH 19/21] add library --- .../electricity-rate-simulator/poetry.lock | 19 ++++++++++++++++++- .../electricity-rate-simulator/pyproject.toml | 1 + 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock index 7b4d6e58c..286d31bc0 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/poetry.lock @@ -485,6 +485,23 @@ tomli = {version = ">=1.0.0", markers = "python_version < \"3.11\""} [package.extras] testing = ["argcomplete", "attrs (>=19.2.0)", "hypothesis (>=3.56)", "mock", "nose", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +[[package]] +name = "pytest-mock" +version = "3.12.0" +description = "Thin-wrapper around the mock package for easier use with pytest" +optional = false +python-versions = ">=3.8" +files = [ + {file = "pytest-mock-3.12.0.tar.gz", hash = "sha256:31a40f038c22cad32287bb43932054451ff5583ff094bca6f675df2f8bc1a6e9"}, + {file = "pytest_mock-3.12.0-py3-none-any.whl", hash = "sha256:0972719a7263072da3a21c7f4773069bcc7486027d7e8e1f81d98a47e701bc4f"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[package.extras] +dev = ["pre-commit", "pytest-asyncio", "tox"] + [[package]] name = "python-dotenv" version = "1.0.1" @@ -888,4 +905,4 @@ files = [ [metadata] lock-version = "2.0" python-versions = "^3.10" -content-hash = "f1746a6428e2daa98d55268168f19a3e0edac0fe4ef868ecad776373c950ab39" +content-hash = "c7823d8824373f9dbc6fdcf1ee3737933c92792d0d619fbf6beff847eb186294" diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml index f3700ec02..daea55f52 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/pyproject.toml @@ -15,6 +15,7 @@ uvicorn = {extras = ["standard"], version = "^0.27.1"} pytest = "^8.0.0" requests = "^2.31.0" httpx = "^0.26.0" +pytest-mock = "^3.12.0" [build-system] requires = ["poetry-core"] From 41739da3c570726806c74622226111dc7f783e98 Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Wed, 28 Feb 2024 13:51:53 +0000 Subject: [PATCH 20/21] modify exceptions --- .../electricity_rate_simulator/app.py | 8 +- .../electricity_rate_simulator/core.py | 51 +++++-- .../tests/conftest.py | 46 +++++- .../tests/test_core.py | 138 ++++++------------ .../tests/test_model.py | 9 +- 5 files changed, 131 insertions(+), 121 deletions(-) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py index 6275506b5..e01d0ea11 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/app.py @@ -1,9 +1,13 @@ import uvicorn from electricity_rate_simulator.core import ElectricSimulator -from electricity_rate_simulator.exception import ElectricSimulationError +from electricity_rate_simulator.exception import ( + ElectricSimulateClientError, + ElectricSimulationError, +) from electricity_rate_simulator.model import UserData from electricity_rate_simulator.utils import setup_logging from fastapi import FastAPI, HTTPException +from pydantic import ValidationError app = FastAPI() @@ -20,7 +24,7 @@ def electric_simulations_api(contract: int, usage: int): try: user_data = UserData(contract=contract, usage=usage) - except ElectricSimulationError as e: + except (ElectricSimulateClientError, ValidationError) as e: raise HTTPException(status_code=400, detail=f"{e}") try: diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py index 6dae83196..db65c05ed 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/electricity_rate_simulator/core.py @@ -42,21 +42,11 @@ def simulate(self, user_data: UserData): return simulations def _validate_profile(self, profile_data: dict): + try: - contracts = [ - PlanContract( - contract=contracts.get("contract"), price=contracts.get("price") - ) - for contracts in profile_data.get("contracts") - ] - usages = [ - PlanUsage( - over=usage.get("over"), - until=usage.get("until"), - price=usage.get("price"), - ) - for usage in profile_data.get("usage") - ] + contracts = self._validate_plan_contract(profile_data.get("contracts")) + usages = self._validate_plan_usage(profile_data.get("usage")) + return ProFile( provider=profile_data.get("provider"), plan=profile_data.get("name"), @@ -64,11 +54,42 @@ def _validate_profile(self, profile_data: dict): usage=usages, ) except ValidationError as e: - raise ElectricSimulationError(e) + raise ElectricSimulateProviderError(e) except ElectricSimulateProviderError as e: raise e + def _validate_plan_contract(self, contracts: list[dict]): + plan_contracts = [] + for contract in contracts: + try: + plan_contract = PlanContract( + contract=contract.get("contract"), price=contract.get("price") + ) + plan_contracts.append(plan_contract) + except ValidationError as e: + raise ElectricSimulateProviderError(e) + + if not plan_contracts: + raise ElectricSimulateProviderError("Not Found plan contracts.") + + return plan_contracts + + def _validate_plan_usage(self, usage): + plan_usages = [] + for item in usage: + plan_usage = PlanUsage( + over=item.get("over"), + until=item.get("until"), + price=item.get("price"), + ) + plan_usages.append(plan_usage) + + if not plan_usages: + raise ElectricSimulateProviderError("Not found plan usages.") + + return plan_usages + def _calculate_electricity_rate(self, provider_data: ProFile, user_data: UserData): try: base_price = self._calculate_base_rate( diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py index 83d96d25c..1f768a853 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/conftest.py @@ -1,7 +1,8 @@ -import pytest import json from pathlib import Path -from electricity_rate_simulator.model import ProFile, PlanContract, PlanUsage + +import pytest +from electricity_rate_simulator.model import PlanContract, PlanUsage, ProFile, UserData BASE_DIR = Path(__file__).parents[1] DATA_DIR = BASE_DIR.joinpath("electricity_rate_simulator", "data") @@ -17,6 +18,23 @@ ) +@pytest.fixture() +def test_user_data(): + return UserData(contract=10, usage=100) + + +@pytest.fixture() +def test_profile(): + with open(TEST_PROFILE, "r", encoding="utf-8") as f: + return json.load(f) + + +@pytest.fixture() +def test_profile_invalid_contracts(): + with open(TEST_PROFILE_INVAILED_CONTRACTS, "r", encoding="utf-8") as f: + return json.load(f) + + @pytest.fixture() def test_plan(): return _test_provider_file(TEST_PROFILE) @@ -33,6 +51,7 @@ def test_contracts(): test_profile = _test_provider_file(TEST_PROFILE) return test_profile.contracts + @pytest.fixture() def test_contracts_invalid_data(): test_profile = _test_provider_file(TEST_PROFILE_INVAILED_CONTRACTS) @@ -77,6 +96,23 @@ def test_plan_by_jxtg_electricity(): def _test_provider_file(profile: Path): with open(profile, "r", encoding="utf-8") as f: profile_data = json.load(f) - contracts = [PlanContract(contract=contracts.get("contract"), price=contracts.get("price")) for contracts in profile_data.get("contracts")] - usages = [PlanUsage(over=usage.get("over"), until=usage.get("until"), price=usage.get("price")) for usage in profile_data.get("usage")] - return ProFile(provider=profile_data.get("provider"), plan=profile_data.get("name"), contracts=contracts, usage=usages) + contracts = [ + PlanContract( + contract=contracts.get("contract"), price=contracts.get("price") + ) + for contracts in profile_data.get("contracts") + ] + usages = [ + PlanUsage( + over=usage.get("over"), + until=usage.get("until"), + price=usage.get("price"), + ) + for usage in profile_data.get("usage") + ] + return ProFile( + provider=profile_data.get("provider"), + plan=profile_data.get("name"), + contracts=contracts, + usage=usages, + ) diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py index 57a0cb619..7408ae4de 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_core.py @@ -3,17 +3,10 @@ from electricity_rate_simulator.exception import ( ElectricSimulateClientError, ElectricSimulateProviderError, - ElectricSimulationError, - InvalidContractError, - InvalidContractsError, - InvalidUsageError, - InvalidUsageOverError, - InvalidUsagePriceError, - InvalidUsagesError, NotFoundContractError, NotFoundProviderError, ) -from electricity_rate_simulator.model import UserData +from electricity_rate_simulator.model import PlanContract, UserData from pytest_mock.plugin import MockerFixture from .conftest import TEST_PROFILE @@ -64,126 +57,85 @@ class TestElectricSimurationExceptions: usage = 100 electric_simuratior = ElectricSimulator() - def test_simulate_invalid_contract_error(self, mocker: MockerFixture): - err_msg = "dummy InvalidContractError" - mocker.patch( - "electricity_rate_simulator.core.ElectricSimulator.simulate", - side_effect=InvalidContractError(err_msg), - ) - with pytest.raises(ElectricSimulationError) as e: - self.electric_simuratior.simulate(self.contract, self.usage) - - assert str(e.value) == err_msg - - def test_simulate_invalid_usage_error(self, mocker: MockerFixture): - err_msg = "dummy InvalidUsageError" - mocker.patch( - "electricity_rate_simulator.core.ElectricSimulator.simulate", - side_effect=InvalidUsageError(err_msg), - ) - with pytest.raises(InvalidUsageError) as e: - self.electric_simuratior.simulate(self.contract, self.usage) - - assert str(e.value) == err_msg - - def test_simulate_notfound_provider_error(self, mocker: MockerFixture): - err_msg = "dummy NotFoundProviderError" + def test_simulate_notfound_provider_error_by_validate_profile( + self, mocker: MockerFixture + ): mocker.patch( - "electricity_rate_simulator.core.ElectricSimulator.simulate", - side_effect=NotFoundProviderError("dummy NotFoundProviderError"), + "electricity_rate_simulator.core.ElectricSimulator._validate_profile", + side_effect=ElectricSimulateProviderError( + "dummy ElectricSimulateProviderError" + ), ) with pytest.raises(NotFoundProviderError) as e: - self.electric_simuratior.simulate() + user_data = UserData(contract=10, usage=10) + self.electric_simuratior.simulate(user_data) - assert str(e.value) == err_msg + assert str(e.value) == "Not Found providers" - def test_calculate_electricity_rate_invalid_contracts_error( + def test_simulate_notfound_provider_error_by_calculate_electricity_rate( self, mocker: MockerFixture ): - err_msg = "dummy InvalidContractsError" mocker.patch( "electricity_rate_simulator.core.ElectricSimulator._calculate_electricity_rate", - side_effect=InvalidContractsError(err_msg), + side_effect=ElectricSimulateClientError( + "dummy ElectricSimulateClientError" + ), ) - with pytest.raises(ElectricSimulateProviderError) as e: - self.electric_simuratior._calculate_electricity_rate(self.profile) - assert str(e.value) == err_msg + with pytest.raises(NotFoundProviderError) as e: + user_data = UserData(contract=10, usage=10) + self.electric_simuratior.simulate(user_data) - def test_calculate_electricity_rate_invalid_usages_error( - self, mocker: MockerFixture - ): - err_msg = "dummy InvalidUsagesError" - mocker.patch( - "electricity_rate_simulator.core.ElectricSimulator._calculate_electricity_rate", - side_effect=InvalidUsagesError(err_msg), - ) - with pytest.raises(ElectricSimulateProviderError) as e: - self.electric_simuratior._calculate_electricity_rate(self.profile) - assert str(e.value) == err_msg + assert str(e.value) == "Not Found providers" - def test_calculate_electricity_rate_invalid_usage_error( - self, mocker: MockerFixture + def test_validate_profile_by_validation_error_plan_contract( + self, mocker: MockerFixture, test_profile ): err_msg = "dummy ElectricSimulateProviderError" mocker.patch( - "electricity_rate_simulator.core.ElectricSimulator._calculate_electricity_rate", + "electricity_rate_simulator.core.ElectricSimulator._validate_plan_contract", side_effect=ElectricSimulateProviderError(err_msg), ) with pytest.raises(ElectricSimulateProviderError) as e: - self.electric_simuratior._calculate_electricity_rate(self.profile) + self.electric_simuratior._validate_profile(test_profile) assert str(e.value) == err_msg - def test_calculate_electricity_rate_notfound_contract_error( - self, mocker: MockerFixture + def test_validate_profile_by_validation_error_plan_usage( + self, mocker: MockerFixture, test_profile ): - err_msg = "dummy ElectricSimulateClientError" + err_msg = "dummy ElectricSimulateProviderError" mocker.patch( - "electricity_rate_simulator.core.ElectricSimulator._calculate_electricity_rate", - side_effect=ElectricSimulateClientError(err_msg), + "electricity_rate_simulator.core.ElectricSimulator._validate_plan_usage", + side_effect=ElectricSimulateProviderError(err_msg), ) - with pytest.raises(ElectricSimulateClientError) as e: - self.electric_simuratior._calculate_electricity_rate(self.profile) + with pytest.raises(ElectricSimulateProviderError) as e: + self.electric_simuratior._validate_profile(test_profile) assert str(e.value) == err_msg - def test_calculate_base_rate_notfound_contract_error(self, mocker: MockerFixture): + def test_calculate_electricity_rate_Not_found_contract_error( + self, mocker: MockerFixture, test_plan, test_user_data + ): err_msg = "dummy NotFoundContractError" mocker.patch( "electricity_rate_simulator.core.ElectricSimulator._calculate_base_rate", side_effect=NotFoundContractError(err_msg), ) - contracts = [{}] - with pytest.raises(NotFoundContractError) as e: - self.electric_simuratior._calculate_base_rate(contracts) - - assert str(e.value) == err_msg - - def test_calculate_usage_rate_invalid_usage_over_error(self, mocker: MockerFixture): - err_msg = "dummy InvalidUsageOverError" - mocker.patch( - "electricity_rate_simulator.core.ElectricSimulator._calculate_usage_rate", - side_effect=InvalidUsageOverError(err_msg), - ) - contracts = [{}] - with pytest.raises(InvalidUsageOverError) as e: - self.electric_simuratior._calculate_usage_rate(contracts) - + with pytest.raises(ElectricSimulateClientError) as e: + self.electric_simuratior._calculate_electricity_rate( + test_plan, test_user_data + ) assert str(e.value) == err_msg - def test_calculate_usage_rate_invalid_usage_price_error( - self, mocker: MockerFixture - ): - err_msg = "dummy InvalidUsagePriceError" - mocker.patch( - "electricity_rate_simulator.core.ElectricSimulator._calculate_usage_rate", - side_effect=InvalidUsagePriceError(err_msg), - ) - contracts = [{}] - with pytest.raises(InvalidUsagePriceError) as e: - self.electric_simuratior._calculate_usage_rate(contracts) + def test_calculate_base_rate_notfound_contract_error(self): + test_contract = 10 + contracts = [PlanContract(contract=20, price=100)] + with pytest.raises(NotFoundContractError) as e: + self.electric_simuratior._calculate_base_rate( + contracts, contract=test_contract + ) - assert str(e.value) == err_msg + assert str(e.value) == f"Not found number of contract: {test_contract}" class TestElectricSimurationByTepco: diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py index 90f5ba003..2e5fea99a 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/tests/test_model.py @@ -1,16 +1,14 @@ -from electricity_rate_simulator.model import UserData, PlanUsage, PlanContract - import pytest - from electricity_rate_simulator.exception import ( InvalidContractError, + InvalidContractPriceError, InvalidContractsError, InvalidUsageError, InvalidUsageOverError, - InvalidUsageUntilError, InvalidUsagePriceError, - InvalidContractPriceError, + InvalidUsageUntilError, ) +from electricity_rate_simulator.model import PlanContract, PlanUsage, UserData class TestUserData: @@ -109,4 +107,3 @@ def test_profile(self, test_plan): PlanUsage(over=100, until=200, price=20), PlanUsage(over=300, until=None, price=30), ] - From e14ee686506c75b36ed4deb778740d8c3845ffef Mon Sep 17 00:00:00 2001 From: yukikawamura Date: Wed, 28 Feb 2024 17:18:07 +0000 Subject: [PATCH 21/21] add documents. --- .../electricity-rate-simulator/README.md | 53 ++++++++++++++++-- .../docs/images/api-configuration.png | Bin 0 -> 53249 bytes .../docs/images/application-configuration.png | Bin 0 -> 50437 bytes .../images/electric-simulate-application.png | Bin 0 -> 55434 bytes 4 files changed, 47 insertions(+), 6 deletions(-) create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/api-configuration.png create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/application-configuration.png create mode 100644 serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/electric-simulate-application.png diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/README.md b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/README.md index d22a65860..b2e75f901 100644 --- a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/README.md +++ b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/README.md @@ -1,17 +1,58 @@ # electricity-rate-simulator ## tl;dr -電気料金をシミュレートするbackend api +frontからuser情報を受け取って、電気料金をシミュレートするbackend api -### usage +## 構成 +- applicationの構成 + ![electricity-rate-simulator-image](docs/images/electric-simulate-application.png) +- apiの構成 + ![api-configuration-image](docs/images/api-configuration.png) +- backend-apiの構成 + ![application-configuration-image](docs/images/application-configuration.png) -- request -- response +## 使い方 +### GET /simulations -### for developer +#### request +```url +/simulations?contract=&usage= +``` -``` poetry install ``` \ No newline at end of file +|parameters|type|dscription| +|--|--|--| +|contract|int|契約アンペア数| +|usage|int|使用量| + + +#### response +```json +[{"provider": "会社名", "plan": "plan名", "price": "xxxxxx円"},{...}...] +``` + +## 開発者向け + +### 事前設定 +vscodeの[.devcontaier](https://code.visualstudio.com/docs/devcontainers/containers)機能を用いて開発 + + +### projectのインストール +```bash +$ poetry install +``` + +### server 起動 +```bash +$ cd electricity-rate-simulator/electricity_rate_simulator +$ poetry run python electricity-rate-simulator/electricity_rate_simulator/app.py +``` + +### apiの仕様 +server起動後、以下のurlにアクセス +```bash +/docs +``` diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/api-configuration.png b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/api-configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..acf7bf761cacbc4a780a961ccb7ab805abded7ee GIT binary patch literal 53249 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 MX`qp#W*z+h0CCQ!YXATM literal 0 HcmV?d00001 diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/application-configuration.png b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/application-configuration.png new file mode 100644 index 0000000000000000000000000000000000000000..f070cbfd2d088d663d666c41acb8dcb5eed8fff5 GIT binary patch literal 50437 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 literal 0 HcmV?d00001 diff --git a/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/electric-simulate-application.png b/serverside_challenge_1/challenges/yukikawamura/electricity-rate-simulator/docs/images/electric-simulate-application.png new file mode 100644 index 0000000000000000000000000000000000000000..a55b6a3cbb987912899e53a2740b0810db616f77 GIT binary patch literal 55434 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

3mwcntI;uNs!WHbXqvkc}Z{E^0Z^MQCM zcF)TIkfg*9(tm1WYSL2K4H1kUj+a3k4N9#$<=tg)hyC}0|MYB zChje){C3Y6LGqi@G{sRxMCm=nM@?XeZ-D=sXkZ?wHyH3vqhpvv`a|uV&{ta$885lx z(=(SB*_T#d1wTJ>s0E&xd=TuM%0NtYJ+60T30x0`PB_8u+))<60zXHTG!^sYEkgev DhjDD~ literal 0 HcmV?d00001